diff --git a/.eslintrc.json b/.eslintrc.json index 671e7b2fab..61bebbf483 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -246,6 +246,22 @@ } ] } + }, + { + "files": ["**/*.ts"], + "excludedFiles": ["**/platform/**/*.ts"], + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + "**/platform/**/internal", // General internal pattern + // All features that have been converted to barrel files + "**/platform/messaging/**" + ] + } + ] + } } ] } diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 585a888ae1..23f4bd35f1 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -160,9 +160,9 @@ jobs: run: npm run dist working-directory: browser-source/apps/browser - # - name: Build Manifest v3 - # run: npm run dist:mv3 - # working-directory: browser-source/apps/browser + - name: Build Manifest v3 + run: npm run dist:mv3 + working-directory: browser-source/apps/browser - name: Gulp run: gulp ci @@ -189,12 +189,12 @@ jobs: path: browser-source/apps/browser/dist/dist-chrome.zip if-no-files-found: error - # - name: Upload Chrome MV3 artifact - # uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 - # with: - # name: dist-chrome-MV3-${{ env._BUILD_NUMBER }}.zip - # path: browser-source/apps/browser/dist/dist-chrome-mv3.zip - # if-no-files-found: error + - name: Upload Chrome MV3 artifact (DO NOT USE FOR PROD) + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: DO-NOT-USE-FOR-PROD-dist-chrome-MV3-${{ env._BUILD_NUMBER }}.zip + path: browser-source/apps/browser/dist/dist-chrome-mv3.zip + if-no-files-found: error - name: Upload Firefox artifact uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 769e700588..6a5d9f1405 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -128,29 +128,90 @@ jobs: - name: Success Code run: exit 0 - get-branch-or-tag-sha: - name: Get Branch or Tag SHA + artifact-check: + name: Check if Web artifact is present runs-on: ubuntu-22.04 + needs: setup + env: + _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment-artifact }} outputs: - branch-or-tag-sha: ${{ steps.get-branch-or-tag-sha.outputs.sha }} + artifact-build-commit: ${{ steps.set-artifact-commit.outputs.commit }} steps: - - name: Checkout Branch - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}' + if: ${{ inputs.build-web-run-id }} + uses: bitwarden/gh-actions/download-artifacts@main + id: download-latest-artifacts-run-id + continue-on-error: true with: - ref: ${{ inputs.branch-or-tag }} - fetch-depth: 0 + workflow: build-web.yml + path: apps/web + workflow_conclusion: success + run_id: ${{ inputs.build-web-run-id }} + artifacts: ${{ env._ENVIRONMENT_ARTIFACT }} - - name: Get Branch or Tag SHA - id: get-branch-or-tag-sha + - name: 'Download latest cloud asset from branch/tag: ${{ inputs.branch-or-tag }}' + if: ${{ !inputs.build-web-run-id }} + uses: bitwarden/gh-actions/download-artifacts@main + id: download-latest-artifacts + continue-on-error: true + with: + workflow: build-web.yml + path: apps/web + workflow_conclusion: success + branch: ${{ inputs.branch-or-tag }} + artifacts: ${{ env._ENVIRONMENT_ARTIFACT }} + + - name: Login to Azure + if: ${{ steps.download-latest-artifacts.outcome == 'failure' }} + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve secrets for Build trigger + if: ${{ steps.download-latest-artifacts.outcome == 'failure' }} + id: retrieve-secret + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "github-pat-bitwarden-devops-bot-repo-scope" + + - name: 'Trigger build web for missing branch/tag ${{ inputs.branch-or-tag }}' + if: ${{ steps.download-latest-artifacts.outcome == 'failure' }} + uses: convictional/trigger-workflow-and-wait@f69fa9eedd3c62a599220f4d5745230e237904be # v1.6.5 + id: trigger-build-web + with: + owner: bitwarden + repo: clients + github_token: ${{ steps.retrieve-secret.outputs.github-pat-bitwarden-devops-bot-repo-scope }} + workflow_file_name: build-web.yml + ref: ${{ inputs.branch-or-tag }} + wait_interval: 100 + + - name: Set artifact build commit + id: set-artifact-commit + env: + GH_TOKEN: ${{ github.token }} run: | - echo "sha=$(git rev-parse origin/${{ inputs.branch-or-tag }})" >> $GITHUB_OUTPUT + # If run-id was used, get the commit from the download-latest-artifacts-run-id step + if [ "${{ inputs.build-web-run-id }}" ]; then + echo "commit=${{ steps.download-latest-artifacts-run-id.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT + + elif [ "${{ steps.download-latest-artifacts.outcome }}" == "failure" ]; then + # If the download-latest-artifacts step failed, query the GH API to get the commit SHA of the artifact that was just built with trigger-build-web. + commit=$(gh api /repos/bitwarden/clients/actions/runs/${{ steps.trigger-build-web.outputs.workflow_id }}/artifacts --jq '.artifacts[0].workflow_run.head_sha') + echo "commit=$commit" >> $GITHUB_OUTPUT + + else + # Set the commit to the output of step download-latest-artifacts. + echo "commit=${{ steps.download-latest-artifacts.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT + fi notify-start: name: Notify Slack with start message needs: - approval - setup - - get-branch-or-tag-sha + - artifact-check runs-on: ubuntu-22.04 if: ${{ always() && contains( inputs.environment , 'QA' ) }} outputs: @@ -165,66 +226,10 @@ jobs: tag: ${{ inputs.branch-or-tag }} slack-channel: team-eng-qa-devops event: 'start' - commit-sha: ${{ needs.get-branch-or-tag-sha.outputs.branch-or-tag-sha }} + commit-sha: ${{ needs.artifact-check.outputs.artifact-build-commit }} url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }} AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - artifact-check: - name: Check if Web artifact is present - runs-on: ubuntu-22.04 - needs: setup - env: - _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment-artifact }} - steps: - - name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}' - if: ${{ inputs.build-web-run-id }} - uses: bitwarden/gh-actions/download-artifacts@main - id: download-latest-artifacts - continue-on-error: true - with: - workflow: build-web.yml - path: apps/web - workflow_conclusion: success - run_id: ${{ inputs.build-web-run-id }} - artifacts: ${{ env._ENVIRONMENT_ARTIFACT }} - - - name: 'Download latest cloud asset from branch/tag: ${{ inputs.branch-or-tag }}' - if: ${{ !inputs.build-web-run-id }} - uses: bitwarden/gh-actions/download-artifacts@main - id: download-artifacts - continue-on-error: true - with: - workflow: build-web.yml - path: apps/web - workflow_conclusion: success - branch: ${{ inputs.branch-or-tag }} - artifacts: ${{ env._ENVIRONMENT_ARTIFACT }} - - - name: Login to Azure - if: ${{ steps.download-artifacts.outcome == 'failure' }} - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Retrieve secrets for Build trigger - if: ${{ steps.download-artifacts.outcome == 'failure' }} - id: retrieve-secret - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: "bitwarden-ci" - secrets: "github-pat-bitwarden-devops-bot-repo-scope" - - - name: 'Trigger build web for missing branch/tag ${{ inputs.branch-or-tag }}' - if: ${{ steps.download-artifacts.outcome == 'failure' }} - uses: convictional/trigger-workflow-and-wait@f69fa9eedd3c62a599220f4d5745230e237904be # v1.6.5 - with: - owner: bitwarden - repo: clients - github_token: ${{ steps.retrieve-secret.outputs.github-pat-bitwarden-devops-bot-repo-scope }} - workflow_file_name: build-web.yml - ref: ${{ inputs.branch-or-tag }} - wait_interval: 100 - azure-deploy: name: Deploy Web Vault to ${{ inputs.environment }} Storage Account needs: @@ -248,6 +253,7 @@ jobs: environment: ${{ env._ENVIRONMENT_NAME }} task: 'deploy' description: 'Deployment from branch/tag: ${{ inputs.branch-or-tag }}' + ref: ${{ needs.artifact-check.outputs.artifact-build-commit }} - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -349,10 +355,10 @@ jobs: runs-on: ubuntu-22.04 if: ${{ always() && contains( inputs.environment , 'QA' ) }} needs: + - setup - notify-start - azure-deploy - - setup - - get-branch-or-tag-sha + - artifact-check steps: - uses: bitwarden/gh-actions/report-deployment-status-to-slack@main with: @@ -362,6 +368,6 @@ jobs: slack-channel: ${{ needs.notify-start.outputs.channel_id }} event: ${{ needs.azure-deploy.result }} url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }} - commit-sha: ${{ needs.get-branch-or-tag-sha.outputs.branch-or-tag-sha }} + commit-sha: ${{ needs.artifact-check.outputs.artifact-build-commit }} update-ts: ${{ needs.notify-start.outputs.ts }} AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} diff --git a/.storybook/main.ts b/.storybook/main.ts index 544beb48c7..c71a74c2a7 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -38,9 +38,7 @@ const config: StorybookConfig = { }, env: (config) => ({ ...config, - FLAGS: JSON.stringify({ - secretsManager: true, - }), + FLAGS: JSON.stringify({}), }), webpackFinal: async (config, { configType }) => { if (config.resolve) { diff --git a/apps/browser/package.json b/apps/browser/package.json index d06eadf58d..ee6d100572 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.3.1", + "version": "2024.4.1", "scripts": { "build": "webpack", "build:mv3": "cross-env MANIFEST_VERSION=3 webpack", diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 5e3a0152a5..e08894be0b 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - مدير كلمات مرور مجاني", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "مدير كلمات مرور مجاني وآمن لجميع أجهزتك.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "تغيير كلمة المرور الرئيسية" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "عبارة بصمة الإصبع", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "أُضيف المجلد" }, - "changeMasterPass": { - "message": "تغيير كلمة المرور الرئيسية" - }, - "changeMasterPasswordConfirmation": { - "message": "يمكنك تغيير كلمة المرور الرئيسية من خزنة الويب في bitwarden.com. هل تريد زيارة الموقع الآن؟" - }, "twoStepLoginConfirmation": { "message": "تسجيل الدخول بخطوتين يجعل حسابك أكثر أمنا من خلال مطالبتك بالتحقق من تسجيل الدخول باستخدام جهاز آخر مثل مفتاح الأمان، تطبيق المصادقة، الرسائل القصيرة، المكالمة الهاتفية، أو البريد الإلكتروني. يمكن تمكين تسجيل الدخول بخطوتين على خزنة الويب bitwarden.com. هل تريد زيارة الموقع الآن؟" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 5d17d567fc..1e5062d8c6 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Ödənişsiz Parol Meneceri", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bütün cihazlarınız üçün güvənli və ödənişsiz bir parol meneceri.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Ana parolu dəyişdir" }, + "continueToWebApp": { + "message": "Veb tətbiqlə davam edilsin?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Ana parolunuzu Bitwarden veb tətbiqində dəyişdirə bilərsiniz." + }, "fingerprintPhrase": { "message": "Barmaq izi ifadəsi", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Qovluq əlavə edildi" }, - "changeMasterPass": { - "message": "Ana parolu dəyişdir" - }, - "changeMasterPasswordConfirmation": { - "message": "Ana parolunuzu bitwarden.com veb anbarında dəyişdirə bilərsiniz. İndi saytı ziyarət etmək istəyirsiniz?" - }, "twoStepLoginConfirmation": { "message": "İki addımlı giriş, güvənlik açarı, kimlik doğrulayıcı tətbiq, SMS, telefon zəngi və ya e-poçt kimi digər cihazlarla girişinizi doğrulamanızı tələb edərək hesabınızı daha da güvənli edir. İki addımlı giriş, bitwarden.com veb anbarında qurula bilər. Veb saytı indi ziyarət etmək istəyirsiniz?" }, @@ -1045,7 +1045,7 @@ "message": "Bildiriş server URL-si" }, "iconsUrl": { - "message": "Nişan server URL-si" + "message": "İkon server URL-si" }, "environmentSaved": { "message": "Mühit URL-ləri saxlanıldı." @@ -1072,7 +1072,7 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "Avto-doldurma nişanı seçiləndə", + "message": "Avto-doldurma ikonu seçiləndə", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoad": { @@ -1109,7 +1109,7 @@ "message": "Anbarı açılan pəncərədə aç" }, "commandOpenSidebar": { - "message": "Anbar yan sətirdə aç" + "message": "Anbarı yan çubuqda aç" }, "commandAutofillDesc": { "message": "Hazırkı veb sayt üçün son istifadə edilən giriş məlumatlarını avto-doldur" @@ -1162,7 +1162,7 @@ "message": "Bu brauzer bu açılan pəncərədə U2F tələblərini emal edə bilmir. U2F istifadə edərək giriş etmək üçün bu açılan pəncərəni yeni bir pəncərədə açmaq istəyirsiniz?" }, "enableFavicon": { - "message": "Veb sayt nişanlarını göstər" + "message": "Veb sayt ikonlarını göstər" }, "faviconDesc": { "message": "Hər girişin yanında tanına bilən təsvir göstər." @@ -1724,7 +1724,7 @@ "message": "İcazə tələb xətası" }, "nativeMessaginPermissionSidebarDesc": { - "message": "Bu əməliyyatı kənar çubuqda icra edilə bilməz. Lütfən açılan pəncərədə yenidən sınayın." + "message": "Bu əməliyyat yan çubuqda icra edilə bilməz. Lütfən açılan pəncərədə yenidən sınayın." }, "personalOwnershipSubmitError": { "message": "Müəssisə Siyasətinə görə, elementləri şəxsi anbarınızda saxlamağınız məhdudlaşdırılıb. Sahiblik seçimini təşkilat olaraq dəyişdirin və mövcud kolleksiyalar arasından seçim edin." @@ -1924,10 +1924,10 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLinuxChromiumFileWarning": { - "message": "Bir fayl seçmək üçün (mümkünsə) kənar çubuqdakı uzantını açın və ya bu bannerə klikləyərək yeni bir pəncərədə açın." + "message": "Bir fayl seçmək üçün (mümkünsə) yan çubuqdakı uzantını açın və ya bu bannerə klikləyərək yeni bir pəncərədə açın." }, "sendFirefoxFileWarning": { - "message": "Firefox istifadə edərək bir fayl seçmək üçün kənar çubuqdakı uzantını açın və ya bu bannerə klikləyərək yeni bir pəncərədə açın." + "message": "Firefox istifadə edərək bir fayl seçmək üçün yan çubuqdakı uzantını açın və ya bu bannerə klikləyərək yeni bir pəncərədə açın." }, "sendSafariFileWarning": { "message": "Safari istifadə edərək bir fayl seçmək üçün bu bannerə klikləyərək yeni bir pəncərədə açın." @@ -2706,7 +2706,7 @@ "message": "Hesabınız üçün Duo iki addımlı giriş tələb olunur." }, "popoutTheExtensionToCompleteLogin": { - "message": "Popout the extension to complete login." + "message": "Girişi tamamlamaq üçün uzantını aç." }, "popoutExtension": { "message": "Popout uzantısı" @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Kimlik məlumatlarını saxlama xətası. Detallar üçün konsolu yoxlayın.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Uğurlu" + }, + "removePasskey": { + "message": "Parolu sil" + }, + "passkeyRemoved": { + "message": "Parol silindi" + }, + "unassignedItemsBannerNotice": { + "message": "Bildiriş: Təyin edilməyən təşkilat elementləri artıq Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Bildiriş: 16 May 2024-cü il tarixindən etibarən, təyin edilməyən təşkilat elementləri Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Bu elementləri görünən etmək üçün", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "bir kolleksiyaya təyin edin.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Konsolu" + }, + "errorAssigningTargetCollection": { + "message": "Hədəf kolleksiyaya təyin etmə xətası." + }, + "errorAssigningTargetFolder": { + "message": "Hədəf qovluğa təyin etmə xətası." } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index f05102d29f..91ff397b3a 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden – бясплатны менеджар пароляў", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Бяспечны і бясплатны менеджар пароляў для ўсіх вашых прылад.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Змяніць асноўны пароль" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Фраза адбітка пальца", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Папка дададзена" }, - "changeMasterPass": { - "message": "Змяніць асноўны пароль" - }, - "changeMasterPasswordConfirmation": { - "message": "Вы можаце змяніць свой асноўны пароль у вэб-сховішчы на bitwarden.com. Перайсці на вэб-сайт зараз?" - }, "twoStepLoginConfirmation": { "message": "Двухэтапны ўваход робіць ваш уліковы запіс больш бяспечным, патрабуючы пацвярджэнне ўваходу на іншай прыладзе з выкарыстаннем ключа бяспекі, праграмы аўтэнтыфікацыі, SMS, тэлефоннага званка або электроннай пошты. Двухэтапны ўваход уключаецца на bitwarden.com. Перайсці на вэб-сайт, каб зрабіць гэта?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index e96a48b3f0..33be2608b4 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -3,11 +3,11 @@ "message": "Битуорден (Bitwarden)" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Безопасно и безплатно управление за всичките ви устройства.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Промяна на главната парола" }, + "continueToWebApp": { + "message": "Продължаване към уеб приложението?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Може да промените главната си парола в уеб приложението на Битуорден." + }, "fingerprintPhrase": { "message": "Уникална фраза", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Добавена папка" }, - "changeMasterPass": { - "message": "Промяна на главната парола" - }, - "changeMasterPasswordConfirmation": { - "message": "Главната парола на трезор може да се промени чрез сайта bitwarden.com. Искате ли да го посетите?" - }, "twoStepLoginConfirmation": { "message": "Двустепенното вписване защитава регистрацията ви, като ви кара да потвърдите влизането си чрез устройство-ключ, приложение за удостоверение, мобилно съобщение, телефонно обаждане или електронна поща. Двустепенното вписване може да се включи чрез сайта bitwarden.com. Искате ли да го посетите?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Грешка при запазването на идентификационните данни. Вижте конзолата за подробности.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Успех" + }, + "removePasskey": { + "message": "Премахване на секретния ключ" + }, + "passkeyRemoved": { + "message": "Секретният ключ е премахнат" + }, + "unassignedItemsBannerNotice": { + "message": "Известие: неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори, а са достъпни само през Административната конзола." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Известие: след 16 май 2024, неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори, а ще бъдат достъпни само през Административната конзола." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Добавете тези елементи към колекция в", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "за да ги направите видими.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Административна конзола" + }, + "errorAssigningTargetCollection": { + "message": "Грешка при задаването на целева колекция." + }, + "errorAssigningTargetFolder": { + "message": "Грешка при задаването на целева папка." } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index cfaed770c1..a12308648a 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "আপনার সমস্ত ডিভাইসের জন্য একটি সুরক্ষিত এবং বিনামূল্যের পাসওয়ার্ড ব্যবস্থাপক।", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "মূল পাসওয়ার্ড পরিবর্তন" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "ফিঙ্গারপ্রিন্ট ফ্রেজ", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "ফোল্ডার জোড়া হয়েছে" }, - "changeMasterPass": { - "message": "মূল পাসওয়ার্ড পরিবর্তন" - }, - "changeMasterPasswordConfirmation": { - "message": "আপনি bitwarden.com ওয়েব ভল্ট থেকে মূল পাসওয়ার্ডটি পরিবর্তন করতে পারেন। আপনি কি এখনই ওয়েবসাইটটি দেখতে চান?" - }, "twoStepLoginConfirmation": { "message": "দ্বি-পদক্ষেপ লগইন অন্য ডিভাইসে আপনার লগইনটি যাচাই করার জন্য সিকিউরিটি কী, প্রমাণীকরণকারী অ্যাপ্লিকেশন, এসএমএস, ফোন কল বা ই-মেইল ব্যাবহারের মাধ্যমে আপনার অ্যাকাউন্টকে আরও সুরক্ষিত করে। bitwarden.com ওয়েব ভল্টে দ্বি-পদক্ষেপের লগইন সক্ষম করা যাবে। আপনি কি এখনই ওয়েবসাইটটি দেখতে চান?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 9260f5c902..7f406fabee 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index b77edf5611..7c8bd63aea 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Administrador de contrasenyes segur i gratuït per a tots els vostres dispositius.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Canvia la contrasenya mestra" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Frase d'empremta digital", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Carpeta afegida" }, - "changeMasterPass": { - "message": "Canvia la contrasenya mestra" - }, - "changeMasterPasswordConfirmation": { - "message": "Podeu canviar la contrasenya mestra a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" - }, "twoStepLoginConfirmation": { "message": "L'inici de sessió en dues passes fa que el vostre compte siga més segur, ja que obliga a verificar el vostre inici de sessió amb un altre dispositiu, com ara una clau de seguretat, una aplicació autenticadora, un SMS, una trucada telefònica o un correu electrònic. Es pot habilitar l'inici de sessió en dues passes a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "S'ha produït un error en guardar les credencials. Consulteu la consola per obtenir més informació.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Suprimeix la clau de pas" + }, + "passkeyRemoved": { + "message": "Clau de pas suprimida" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index bc2d7e8fd8..bd3c6882df 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden – Bezplatný správce hesel", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bezpečný a bezplatný správce hesel pro všechna Vaše zařízení.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Změnit hlavní heslo" }, + "continueToWebApp": { + "message": "Pokračovat do webové aplikace?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hlavní heslo můžete změnit ve webové aplikaci Bitwardenu." + }, "fingerprintPhrase": { "message": "Fráze otisku prstu", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Složka byla přidána" }, - "changeMasterPass": { - "message": "Změnit hlavní heslo" - }, - "changeMasterPasswordConfirmation": { - "message": "Hlavní heslo si můžete změnit na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít?" - }, "twoStepLoginConfirmation": { "message": "Dvoufázové přihlášení činí Váš účet mnohem bezpečnějším díky nutnosti po každém úspěšném přihlášení zadat ověřovací kód získaný z bezpečnostního klíče, aplikace, SMS, telefonního hovoru nebo e-mailu. Dvoufázové přihlášení lze aktivovat na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Chyba při ukládání přihlašovacích údajů. Podrobnosti naleznete v konzoli.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Úspěch" + }, + "removePasskey": { + "message": "Odebrat přístupový klíč" + }, + "passkeyRemoved": { + "message": "Přístupový klíč byl odebrán" + }, + "unassignedItemsBannerNotice": { + "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve vašem zobrazení všech trezorů a jsou nyní přístupné pouze v konzoli správce." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Upozornění: 16. květba 2024 již nebudou nepřiřazené položky organizace viditelné ve vašem zobrazení všech trezorů a budou přístupné pouze v konzoli správce." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Přiřadit tyto položky ke kolekci z", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "aby byly viditelné.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Konzole správce" + }, + "errorAssigningTargetCollection": { + "message": "Chyba při přiřazování cílové kolekce." + }, + "errorAssigningTargetFolder": { + "message": "Chyba při přiřazování cílové složky." } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 665470b512..c718c1d876 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Rheolydd cyfineiriau am ddim", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Rheolydd cyfrineiriau diogel a rhad ac am ddim ar gyfer eich holl ddyfeisiau.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Newid y prif gyfrinair" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Ymadrodd unigryw", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Ffolder wedi'i hychwanegu" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index ab4c4e5b6e..777c3b484f 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Gratis adgangskodemanager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "En sikker og gratis adgangskodemanager til alle dine enheder.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Skift hovedadgangskode" }, + "continueToWebApp": { + "message": "Fortsæt til web-app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hovedadgangskoden kan ændres via Bitwarden web-appen." + }, "fingerprintPhrase": { "message": "Fingeraftrykssætning", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Mappe tilføjet" }, - "changeMasterPass": { - "message": "Skift hovedadgangskode" - }, - "changeMasterPasswordConfirmation": { - "message": "Du kan ændre din hovedadgangskode i bitwarden.com web-boksen. Vil du besøge hjemmesiden nu?" - }, "twoStepLoginConfirmation": { "message": "To-trins login gør din konto mere sikker ved at kræve, at du verificerer dit login med en anden enhed, såsom en sikkerhedsnøgle, autentificeringsapp, SMS, telefonopkald eller e-mail. To-trins login kan aktiveres i bitwarden.com web-boksen. Vil du besøge hjemmesiden nu?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Fejl under import. Tjek konsollen for detaljer.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Gennemført" + }, + "removePasskey": { + "message": "Fjern adgangsnøgle" + }, + "passkeyRemoved": { + "message": "Adgangsnøgle fjernet" + }, + "unassignedItemsBannerNotice": { + "message": "Bemærk: Utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Bemærk: Pr. 16. maj 2024 er utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Tildel disse emner til en samling via", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "for at gøre dem synlige.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin-konsol" + }, + "errorAssigningTargetCollection": { + "message": "Fejl ved tildeling af målsamling." + }, + "errorAssigningTargetFolder": { + "message": "Fejl ved tildeling af målmappe." } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 0406953936..8f2a59af1e 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Kostenloser Passwortmanager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Ein sicherer und kostenloser Passwortmanager für all deine Geräte.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Master-Passwort ändern" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerabdruck-Phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Ordner hinzugefügt" }, - "changeMasterPass": { - "message": "Master-Passwort ändern" - }, - "changeMasterPasswordConfirmation": { - "message": "Du kannst dein Master-Passwort im Bitwarden.com Web-Tresor ändern. Möchtest du die Seite jetzt öffnen?" - }, "twoStepLoginConfirmation": { "message": "Mit der Zwei-Faktor-Authentifizierung wird dein Konto zusätzlich abgesichert, da jede Anmeldung mit einem anderen Gerät wie einem Sicherheitsschlüssel, einer Authentifizierungs-App, einer SMS, einem Anruf oder einer E-Mail verifiziert werden muss. Die Zwei-Faktor-Authentifizierung kann im bitwarden.com Web-Tresor aktiviert werden. Möchtest du die Website jetzt öffnen?" }, @@ -2706,7 +2706,7 @@ "message": "Für dein Konto ist die Duo Zwei-Faktor-Authentifizierung erforderlich." }, "popoutTheExtensionToCompleteLogin": { - "message": "Popout the extension to complete login." + "message": "Koppel die Erweiterung ab, um die Anmeldung abzuschließen." }, "popoutExtension": { "message": "Popout-Erweiterung" @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Fehler beim Speichern der Zugangsdaten. Details in der Konsole.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Passkey entfernen" + }, + "passkeyRemoved": { + "message": "Passkey entfernt" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Fehler beim Zuweisen der Ziel-Sammlung." + }, + "errorAssigningTargetFolder": { + "message": "Fehler beim Zuweisen des Ziel-Ordners." } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 64014298e2..8c65e61e53 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Δωρεάν Διαχειριστής Κωδικών", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Ένας ασφαλής και δωρεάν διαχειριστής κωδικών, για όλες σας τις συσκευές.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Αλλαγή Κύριου Κωδικού" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Φράση Δακτυλικών Αποτυπωμάτων", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Προστέθηκε φάκελος" }, - "changeMasterPass": { - "message": "Αλλαγή Κύριου Κωδικού" - }, - "changeMasterPasswordConfirmation": { - "message": "Μπορείτε να αλλάξετε τον κύριο κωδικό στο bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" - }, "twoStepLoginConfirmation": { "message": "Η σύνδεση σε δύο βήματα καθιστά πιο ασφαλή τον λογαριασμό σας, απαιτώντας να επαληθεύσετε τα στοιχεία σας με μια άλλη συσκευή, όπως κλειδί ασφαλείας, εφαρμογή επαλήθευσης, μήνυμα SMS, τηλεφωνική κλήση ή email. Μπορείτε να ενεργοποιήσετε τη σύνδεση σε δύο βήματα στο bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index d802d27700..7e6e333689 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -3000,10 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 26af3b5f71..e4d90adf1a 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organisation items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organisation items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index ddbc3f41c9..7cc17240d2 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Added folder" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be enabled on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index b55c64c9f7..3e488bce4c 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden es un gestor de contraseñas seguro y gratuito para todos tus dispositivos.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Cambiar contraseña maestra" }, + "continueToWebApp": { + "message": "¿Continuar a la aplicación web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Puedes cambiar la contraseña maestra en la aplicación web de Bitwarden." + }, "fingerprintPhrase": { "message": "Frase de huella digital", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Carpeta añadida" }, - "changeMasterPass": { - "message": "Cambiar contraseña maestra" - }, - "changeMasterPasswordConfirmation": { - "message": "Puedes cambiar tu contraseña maestra en la caja fuerte web de bitwarden.com. ¿Quieres visitar ahora el sitio web?" - }, "twoStepLoginConfirmation": { "message": "La autenticación en dos pasos hace que tu cuenta sea mucho más segura, requiriendo que introduzcas un código de seguridad de una aplicación de autenticación cada vez que accedes. La autenticación en dos pasos puede ser habilitada en la caja fuerte web de bitwarden.com. ¿Quieres visitar ahora el sitio web?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Se produjo un error al guardar las credenciales. Revise la consola para obtener detalles.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Eliminar passkey" + }, + "passkeyRemoved": { + "message": "Passkey eliminada" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index b7e8b2419e..785a3e4986 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Tasuta paroolihaldur", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Turvaline ja tasuta paroolihaldur kõikidele sinu seadmetele.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Muuda ülemparooli" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Sõrmejälje fraas", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Kaust on lisatud" }, - "changeMasterPass": { - "message": "Muuda ülemparooli" - }, - "changeMasterPasswordConfirmation": { - "message": "Saad oma ülemparooli muuta bitwarden.com veebihoidlas. Soovid seda kohe teha?" - }, "twoStepLoginConfirmation": { "message": "Kaheastmeline kinnitamine aitab konto turvalisust tõsta. Lisaks paroolile pead kontole ligipääsemiseks kinnitama sisselogimise päringu SMS-ga, telefonikõnega, autentimise rakendusega või e-postiga. Kaheastmelist kinnitust saab sisse lülitada bitwarden.com veebihoidlas. Soovid seda kohe avada?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Eemalda pääsuvõti" + }, + "passkeyRemoved": { + "message": "Pääsuvõti on eemaldatud" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index ac30dc4b28..9a07b9d9ae 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Pasahitz kudeatzailea", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden, zure gailu guztietarako pasahitzen kudeatzaile seguru eta doakoa da.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -95,10 +95,10 @@ "message": "Auto-fill login" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "Auto-bete txartela" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "Auto-bete nortasuna" }, "generatePasswordCopied": { "message": "Sortu pasahitza (kopiatuta)" @@ -110,19 +110,19 @@ "message": "Bat datozen saio-hasierarik gabe" }, "noCards": { - "message": "No cards" + "message": "Txartelik ez" }, "noIdentities": { - "message": "No identities" + "message": "Nortasunik ez" }, "addLoginMenu": { "message": "Add login" }, "addCardMenu": { - "message": "Add card" + "message": "Gehitu txartela" }, "addIdentityMenu": { - "message": "Add identity" + "message": "Gehitu nortasuna" }, "unlockVaultMenu": { "message": "Desblokeatu kutxa gotorra" @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Aldatu pasahitz nagusia" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Hatz-marka digitalaren esaldia", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -223,10 +229,10 @@ "message": "Bitwarden Laguntza zentroa" }, "communityForums": { - "message": "Explore Bitwarden community forums" + "message": "Esploratu Bitwarden komunitatearen foroak" }, "contactSupport": { - "message": "Contact Bitwarden support" + "message": "Jarri harremanetan Bitwardeneko laguntza taldearekin" }, "sync": { "message": "Sinkronizatu" @@ -269,7 +275,7 @@ "message": "Luzera" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "Pasahitzaren gutxieneko luzera" }, "uppercase": { "message": "Letra larria (A-Z)" @@ -557,12 +563,6 @@ "addedFolder": { "message": "Karpeta gehituta" }, - "changeMasterPass": { - "message": "Aldatu pasahitz nagusia" - }, - "changeMasterPasswordConfirmation": { - "message": "Zure pasahitz nagusia alda dezakezu bitwarden.com webgunean. Orain joan nahi duzu webgunera?" - }, "twoStepLoginConfirmation": { "message": "Bi urratseko saio hasiera dela eta, zure kontua seguruagoa da, beste aplikazio/gailu batekin saioa hastea eskatzen baitizu; adibidez, segurtasun-gako, autentifikazio-aplikazio, SMS, telefono dei edo email bidez. Bi urratseko saio hasiera bitwarden.com webgunean aktibatu daiteke. Orain joan nahi duzu webgunera?" }, @@ -1064,7 +1064,7 @@ "message": "Edit browser settings." }, "autofillOverlayVisibilityOff": { - "message": "Off", + "message": "Itzalita", "description": "Overlay setting select option for disabling autofill overlay" }, "autofillOverlayVisibilityOnFieldFocus": { @@ -1592,10 +1592,10 @@ "message": "Ezarri pasahitz nagusia" }, "currentMasterPass": { - "message": "Current master password" + "message": "Oraingo pasahitz nagusia" }, "newMasterPass": { - "message": "New master password" + "message": "Pasahitz nagusi berria" }, "confirmNewMasterPass": { "message": "Confirm new master password" @@ -2266,10 +2266,10 @@ "message": "Please make sure your vault is unlocked and the Fingerprint phrase matches on the other device." }, "resendNotification": { - "message": "Resend notification" + "message": "Berbidali jakinarazpena" }, "viewAllLoginOptions": { - "message": "View all log in options" + "message": "Ikusi erregistro guztiak ezarpenetan" }, "notificationSentDevice": { "message": "A notification has been sent to your device." @@ -2293,13 +2293,13 @@ "message": "Check known data breaches for this password" }, "important": { - "message": "Important:" + "message": "Garrantzitsua:" }, "masterPasswordHint": { "message": "Your master password cannot be recovered if you forget it!" }, "characterMinimum": { - "message": "$LENGTH$ character minimum", + "message": "$LENGTH$ karaktere gutxienez", "placeholders": { "length": { "content": "$1", @@ -2326,7 +2326,7 @@ "message": "Select an item from this screen, or explore other options in settings." }, "gotIt": { - "message": "Got it" + "message": "Ulertuta" }, "autofillSettings": { "message": "Auto-fill settings" @@ -2359,25 +2359,25 @@ "message": "Logging in on" }, "opensInANewWindow": { - "message": "Opens in a new window" + "message": "Leiho berri batean irekitzen da" }, "deviceApprovalRequired": { "message": "Device approval required. Select an approval option below:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Gogoratu gailu hau" }, "uncheckIfPublicDevice": { "message": "Uncheck if using a public device" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Onartu zure beste gailutik" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Eskatu administratzailearen onarpena" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "Onartu pasahitz nagusiarekin" }, "ssoIdentifierRequired": { "message": "Organization SSO identifier is required." @@ -2390,31 +2390,31 @@ "message": "Access denied. You do not have permission to view this page." }, "general": { - "message": "General" + "message": "Orokorra" }, "display": { - "message": "Display" + "message": "Bistaratzea" }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Kontua zuzen sortu da!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "Administratzailearen onarpena eskatuta" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "Zure eskaera zure administratzaileari bidali zaio." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Jakinaraziko zaizu onartzen denean." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Arazoak saioa hasterakoan?" }, "loginApproved": { "message": "Login approved" }, "userEmailMissing": { - "message": "User email missing" + "message": "Erabiltzailearen emaila falta da" }, "deviceTrusted": { "message": "Device trusted" @@ -2540,19 +2540,19 @@ "description": "Notification button text for starting a fileless import." }, "importing": { - "message": "Importing...", + "message": "Inportatzen...", "description": "Notification message for when an import is in progress." }, "dataSuccessfullyImported": { - "message": "Data successfully imported!", + "message": "Datuak zuzen inportatu dira!", "description": "Notification message for when an import has completed successfully." }, "dataImportFailed": { - "message": "Error importing. Check console for details.", + "message": "Errorea gertatu da inportatzean. Begiratu xehetasunak kontsolan.", "description": "Notification message for when an import has failed." }, "importNetworkError": { - "message": "Network error encountered during import.", + "message": "Sareko errorea gertatu da inportatzerakoan.", "description": "Notification message for when an import has failed due to a network error." }, "aliasDomain": { @@ -2602,11 +2602,11 @@ "description": "Screen reader text for when a login item is focused where a partial username is displayed. SR will announce this phrase before reading the text of the partial username" }, "noItemsToShow": { - "message": "No items to show", + "message": "Ez dago elementurik erakusteko", "description": "Text to show in overlay if there are no matching items" }, "newItem": { - "message": "New item", + "message": "Elementu berria", "description": "Button text to display in overlay when there are no matching items" }, "addNewVaultItem": { @@ -2618,32 +2618,32 @@ "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { - "message": "Turn on" + "message": "Piztu" }, "ignore": { - "message": "Ignore" + "message": "Ezikusi" }, "importData": { - "message": "Import data", + "message": "Inportatu datuak", "description": "Used for the header of the import dialog, the import button and within the file-password-prompt" }, "importError": { - "message": "Import error" + "message": "Errorea inportatzerakoan" }, "importErrorDesc": { - "message": "There was a problem with the data you tried to import. Please resolve the errors listed below in your source file and try again." + "message": "Inportatzen saiatu zaren datuekin arazo bat egon da. Mesedez, konpondu ondoren adierazten diren akatsak eta saiatu berriro." }, "resolveTheErrorsBelowAndTryAgain": { - "message": "Resolve the errors below and try again." + "message": "Konpondu beheko akatsak eta saiatu berriro." }, "description": { - "message": "Description" + "message": "Deskribapena" }, "importSuccess": { - "message": "Data successfully imported" + "message": "Datuak zuzen inportatu dira" }, "importSuccessNumberOfItems": { - "message": "A total of $AMOUNT$ items were imported.", + "message": "Guztira $AMOUNT$ elementu inportatu dira.", "placeholders": { "amount": { "content": "$1", @@ -2652,7 +2652,7 @@ } }, "tryAgain": { - "message": "Try again" + "message": "Saiatu berriro" }, "verificationRequiredForActionSetPinToContinue": { "message": "Verification required for this action. Set a PIN to continue." @@ -2661,10 +2661,10 @@ "message": "Set PIN" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Egiaztatu biometria erabiliz" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Baieztapenaren zain" }, "couldNotCompleteBiometrics": { "message": "Could not complete biometrics." @@ -2673,13 +2673,13 @@ "message": "Need a different method?" }, "useMasterPassword": { - "message": "Use master password" + "message": "Erabili pasahitz nagusia" }, "usePin": { - "message": "Use PIN" + "message": "Erabili PIN kodea" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Erabili biometria" }, "enterVerificationCodeSentToEmail": { "message": "Enter the verification code that was sent to your email." @@ -2688,10 +2688,10 @@ "message": "Resend code" }, "total": { - "message": "Total" + "message": "Guztira" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "$ORGANIZATION$(e)ra datuak inportatzen ari zara. Zure datuak erakunde horretako kideekin parteka daitezke. Jarraitu nahi duzu?", "placeholders": { "organization": { "content": "$1", @@ -2810,7 +2810,7 @@ "message": "You do not have a matching login for this site." }, "confirm": { - "message": "Confirm" + "message": "Berretsi" }, "savePasskey": { "message": "Save passkey" @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 373fc5a8d0..c68dc43ef4 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - مدیریت کلمه عبور رایگان", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "یک مدیریت کننده کلمه عبور رایگان برای تمامی دستگاه‌هایتان.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "تغییر کلمه عبور اصلی" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "عبارت اثر انگشت", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "پوشه اضافه شد" }, - "changeMasterPass": { - "message": "تغییر کلمه عبور اصلی" - }, - "changeMasterPasswordConfirmation": { - "message": "شما می‌توانید کلمه عبور اصلی خود را در bitwarden.com تغییر دهید. آیا می‌خواهید از سایت بازدید کنید؟" - }, "twoStepLoginConfirmation": { "message": "ورود دو مرحله ای باعث می‌شود که حساب کاربری شما با استفاده از یک دستگاه دیگر مانند کلید امنیتی، برنامه احراز هویت، پیامک، تماس تلفنی و یا ایمیل، اعتبار خود را با ایمنی بیشتر اثبات کند. ورود دو مرحله ای می تواند در bitwarden.com فعال شود. آیا می‌خواهید از سایت بازدید کنید؟" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 343c22d5d0..2cdb6a2379 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -3,15 +3,15 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden – Ilmainen salasanahallinta", + "message": "Bitwarden – Salasanahallinta", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Turvallinen ja ilmainen salasanahallinta kaikille laitteillesi.", + "message": "Kotona, töissä tai reissussa, Bitwarden suojaa helposti kaikki salasanasi, avainkoodisi ja arkaluonteiset tietosi.", "description": "Extension description" }, "loginOrCreateNewAccount": { - "message": "Käytä salattua holviasi kirjautumalla sisään tai tai luo uusi tili." + "message": "Käytä salattua holviasi kirjautumalla sisään tai luo uusi tili." }, "createAccount": { "message": "Luo tili" @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Vaihda pääsalasana" }, + "continueToWebApp": { + "message": "Avataanko verkkosovellus?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Voit vaihtaa pääsalasanasi Bitwardenin verkkosovelluksessa." + }, "fingerprintPhrase": { "message": "Tunnistelauseke", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Kansio lisätty" }, - "changeMasterPass": { - "message": "Vaihda pääsalasana" - }, - "changeMasterPasswordConfirmation": { - "message": "Voit vaihtaa pääsalasanasi bitwarden.com-verkkoholvissa. Haluatko käydä sivustolla nyt?" - }, "twoStepLoginConfirmation": { "message": "Kaksivaiheinen kirjautuminen parantaa tilisi suojausta vaatimalla kirjautumisen vahvistuksen salasanan lisäksi todennuslaitteen, ‑sovelluksen, tekstiviestin, puhelun tai sähköpostin avulla. Voit ottaa kaksivaiheisen kirjautumisen käyttöön bitwarden.com‑verkkoholvissa. Haluatko avata sen nyt?" }, @@ -802,7 +802,7 @@ "message": "Lue lisää" }, "authenticatorKeyTotp": { - "message": "Todennusaavain (TOTP)" + "message": "Todennusavain (TOTP)" }, "verificationCodeTotp": { "message": "Todennuskoodi (TOTP)" @@ -2989,15 +2989,47 @@ "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { - "message": "Käyttäjätiedot on tallennettu!", + "message": "Käyttäjätiedot tallennettiin!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { - "message": "Käyttäjätiedot on päivitetty!", + "message": "Käyttäjätiedot päivitettiin!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { "message": "Virhe tallennettaessa käyttäjätietoja. Näet isätietoja hallinnasta.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Onnistui" + }, + "removePasskey": { + "message": "Poista suojausavain" + }, + "passkeyRemoved": { + "message": "Suojausavain poistettiin" + }, + "unassignedItemsBannerNotice": { + "message": "Huomioi: Määrittämättömät organisaatiokohteet eivät enää näy \"Kaikki holvit\" -näkymässä, vaan ne ovat käytettävissä vain Hallintapaneelista." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Huomioi: Alkaen 16. toukokuuta 2024, määrittämättömät organisaatiokohteet eivät enää näy \"Kaikki holvit\" -näkymässä, vaan ne ovat käytettävissä vain Hallintapaneelista." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Määritä nämä kohteet kokoelmaan", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": ", jotta ne näkyvät.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Hallintapaneelista" + }, + "errorAssigningTargetCollection": { + "message": "Virhe määritettäessä kohdekokoelmaa." + }, + "errorAssigningTargetFolder": { + "message": "Virhe määritettäessä kohdekansiota." } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 2a09430c27..0dfb4a39c9 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Libreng Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Isang ligtas at libreng password manager para sa lahat ng iyong mga aparato.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Baguhin ang Master Password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Hulmabig ng Hilik ng Dako", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Idinagdag na folder" }, - "changeMasterPass": { - "message": "Palitan ang master password" - }, - "changeMasterPasswordConfirmation": { - "message": "Maaari mong palitan ang iyong master password sa bitwarden.com web vault. Gusto mo bang bisitahin ang website ngayon?" - }, "twoStepLoginConfirmation": { "message": "Ang two-step login ay nagpapagaan sa iyong account sa pamamagitan ng pag-verify sa iyong login sa isa pang device tulad ng security key, authenticator app, SMS, tawag sa telepono o email. Ang two-step login ay maaaring magawa sa bitwarden.com web vault. Gusto mo bang bisitahin ang website ngayon?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 73e00ba489..742e31ee58 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Gestion des mots de passe", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Un gestionnaire de mots de passe sécurisé et gratuit pour tous vos appareils.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Changer le mot de passe principal" }, + "continueToWebApp": { + "message": "Poursuivre vers l'application web ?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Vous pouvez modifier votre mot de passe principal sur l'application web de Bitwarden." + }, "fingerprintPhrase": { "message": "Phrase d'empreinte", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -525,13 +531,13 @@ "message": "Impossible de scanner le QR code à partir de la page web actuelle" }, "totpCaptureSuccess": { - "message": "Clé de l'Authentificateur ajoutée" + "message": "Clé Authenticator ajoutée" }, "totpCapture": { "message": "Scanner le QR code de l'authentificateur à partir de la page web actuelle" }, "copyTOTP": { - "message": "Copier la clé de l'Authentificateur (TOTP)" + "message": "Copier la clé Authenticator (TOTP)" }, "loggedOut": { "message": "Déconnecté" @@ -557,12 +563,6 @@ "addedFolder": { "message": "Dossier ajouté" }, - "changeMasterPass": { - "message": "Changer le mot de passe principal" - }, - "changeMasterPasswordConfirmation": { - "message": "Vous pouvez changer votre mot de passe principal depuis le coffre web de bitwarden.com. Voulez-vous visiter le site web maintenant ?" - }, "twoStepLoginConfirmation": { "message": "L'authentification à deux facteurs rend votre compte plus sûr en vous demandant de vérifier votre connexion avec un autre dispositif tel qu'une clé de sécurité, une application d'authentification, un SMS, un appel téléphonique ou un courriel. L'authentification à deux facteurs peut être configurée dans le coffre web de bitwarden.com. Voulez-vous visiter le site web maintenant ?" }, @@ -659,7 +659,7 @@ "message": "Liste les éléments des cartes de paiement sur la Page d'onglet pour faciliter la saisie automatique." }, "showIdentitiesCurrentTab": { - "message": "Afficher les identités sur la page d'onglet" + "message": "Afficher les identités sur la Page d'onglet" }, "showIdentitiesCurrentTabDesc": { "message": "Liste les éléments d'identité sur la Page d'onglet pour faciliter la saisie automatique." @@ -802,7 +802,7 @@ "message": "En savoir plus" }, "authenticatorKeyTotp": { - "message": "Clé de l'Authentificateur (TOTP)" + "message": "Clé Authenticator (TOTP)" }, "verificationCodeTotp": { "message": "Code de vérification (TOTP)" @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Erreur lors de l'enregistrement des identifiants. Consultez la console pour plus de détails.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Retirer la clé d'identification (passkey)" + }, + "passkeyRemoved": { + "message": "Clé d'identification (passkey) retirée" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 4a37361f29..b4c151eeb0 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -3,39 +3,39 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, "createAccount": { - "message": "Create account" + "message": "Crea unha conta" }, "login": { - "message": "Log in" + "message": "Iniciar sesión" }, "enterpriseSingleSignOn": { "message": "Enterprise single sign-on" }, "cancel": { - "message": "Cancel" + "message": "Cancelar" }, "close": { - "message": "Close" + "message": "Pechar" }, "submit": { "message": "Submit" }, "emailAddress": { - "message": "Email address" + "message": "Enderezo de correo electrónico" }, "masterPass": { - "message": "Master password" + "message": "Contrasinal mestre" }, "masterPassDesc": { "message": "The master password is the password you use to access your vault. It is very important that you do not forget your master password. There is no way to recover the password in the event that you forget it." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index d902be6af0..61482da54a 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - מנהל סיסמאות חינמי", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "מנהל סיסמאות חינמי ומאובטח עבור כל המכשירים שלך.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "החלף סיסמה ראשית" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "סיסמת טביעת אצבע", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "נוספה תיקייה" }, - "changeMasterPass": { - "message": "החלף סיסמה ראשית" - }, - "changeMasterPasswordConfirmation": { - "message": "באפשרותך לשנות את הסיסמה הראשית שלך דרך הכספת באתר bitwarden.com. האם ברצונך לפתוח את האתר כעת?" - }, "twoStepLoginConfirmation": { "message": "התחברות בשני-שלבים הופכת את החשבון שלך למאובטח יותר בכך שאתה נדרש לוודא בכל כניסה בעזרת מכשיר אחר כדוגמת מפתח אבטחה, תוכנת אימות, SMS, שיחת טלפון, או אימייל. ניתן להפעיל את \"התחברות בשני-שלבים\" בכספת שבאתר bitwarden.com. האם ברצונך לפתוח את האתר כעת?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 767edcfb95..b76405eed8 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -3,11 +3,11 @@ "message": "bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "bitwarden is a secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change Master Password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint Phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "जोड़ा गया फ़ोल्डर" }, - "changeMasterPass": { - "message": "Change Master Password" - }, - "changeMasterPasswordConfirmation": { - "message": "आप वेब वॉल्ट bitwarden.com पर अपना मास्टर पासवर्ड बदल सकते हैं।क्या आप अब वेबसाइट पर जाना चाहते हैं?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to enter a security code from an authenticator app whenever you log in. Two-step login can be enabled on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index b79477d83c..2dc500bc1e 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - besplatni upravitelj lozinki", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden je siguran i besplatan upravitelj lozinki za sve tvoje uređaje.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Promjeni glavnu lozinku" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Jedinstvena fraza", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Mapa dodana" }, - "changeMasterPass": { - "message": "Promjeni glavnu lozinku" - }, - "changeMasterPasswordConfirmation": { - "message": "Svoju glavnu lozinku možeš promijeniti na web trezoru. Želiš li sada posjetiti bitwarden.com?" - }, "twoStepLoginConfirmation": { "message": "Prijava dvostrukom autentifikacijom čini tvoj račun još sigurnijim tako što će zahtijevati da potvrdiš prijavu putem drugog uređaja pomoću sigurnosnog koda, autentifikatorske aplikacije, SMS-om, pozivom ili e-poštom. Prijavu dvostrukom autentifikacijom možeš omogućiti na web trezoru. Želiš li sada posjetiti bitwarden.com?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index fe4292d9c0..e47f2cda1f 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Ingyenes jelszókezelő", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Egy biztonságos és ingyenes jelszókezelő az összes eszközre.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Mesterjelszó módosítása" }, + "continueToWebApp": { + "message": "Tovább a webes alkalmazáshoz?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "A mesterjelszó a Bitwarden webalkalmazásban módosítható." + }, "fingerprintPhrase": { "message": "Ujjlenyomat kifejezés", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "A mappa hozzáadásra került." }, - "changeMasterPass": { - "message": "Mesterjelszó módosítása" - }, - "changeMasterPasswordConfirmation": { - "message": "Mesterjelszavadat a bitwarden.com webes széfén tudod megváltoztatni. Szeretnéd meglátogatni a most a weboldalt?" - }, "twoStepLoginConfirmation": { "message": "A kétlépcsős bejelentkezés biztonságosabbá teszi a fiókot azáltal, hogy ellenőrizni kell a bejelentkezést egy másik olyan eszközzel mint például biztonsági kulcs, hitelesítő alkalmazás, SMS, telefon hívás vagy email. A kétlépcsős bejelentkezést a bitwarden.com webes széfben lehet engedélyezni. Felkeressük a webhelyet most?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Hiba történt a hitelesítések mentésekor. A részletekért ellenőrizzük a konzolt.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Sikeres" + }, + "removePasskey": { + "message": "Jelszó eltávolítása" + }, + "passkeyRemoved": { + "message": "A jelszó eltávolításra került." + }, + "unassignedItemsBannerNotice": { + "message": "Megjegyzés: A nem hozzárendelt szervezeti elemek már nem láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátori konzolon keresztül érhetők el." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Megjegyzés: 2024. május 16-tól a nem hozzárendelt szervezeti elemek többé nem lesznek láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátori konzolon keresztül lesznek elérhetők." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Rendeljük hozzá ezeket az elemeket a gyűjteményhez", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "a láthatósághoz.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Adminisztrátori konzol" + }, + "errorAssigningTargetCollection": { + "message": "Hiba történt a célgyűjtemény hozzárendelése során." + }, + "errorAssigningTargetFolder": { + "message": "Hiba történt a célmappa hozzárendelése során." } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 44f6be8cef..d4399d8e15 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Pengelola Sandi Gratis", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden adalah sebuah pengelola sandi yang aman dan gratis untuk semua perangkat Anda.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Ubah Kata Sandi Utama" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Frasa Sidik Jari", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Tambah Folder" }, - "changeMasterPass": { - "message": "Ubah Kata Sandi Utama" - }, - "changeMasterPasswordConfirmation": { - "message": "Anda dapat mengubah kata sandi utama Anda di brankas web bitwarden.com. Anda ingin mengunjungi situs web sekarang?" - }, "twoStepLoginConfirmation": { "message": "Info masuk dua langkah membuat akun Anda lebih aman dengan mengharuskan Anda memverifikasi info masuk Anda dengan peranti lain seperti kode keamanan, aplikasi autentikasi, SMK, panggilan telepon, atau email. Info masuk dua langkah dapat diaktifkan di brankas web bitwarden.com. Anda ingin mengunjungi situs web sekarang?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index ac6bcc9cb5..93ae682190 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Password Manager Gratis", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Un password manager sicuro e gratis per tutti i tuoi dispositivi.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Cambia password principale" }, + "continueToWebApp": { + "message": "Passa al sito web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Puoi modificare la tua password principale sul sito web di Bitwarden." + }, "fingerprintPhrase": { "message": "Frase impronta", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Cartella aggiunta" }, - "changeMasterPass": { - "message": "Cambia password principale" - }, - "changeMasterPasswordConfirmation": { - "message": "Puoi cambiare la tua password principale sulla cassaforte online di bitwarden.com. Vuoi visitare ora il sito?" - }, "twoStepLoginConfirmation": { "message": "La verifica in due passaggi rende il tuo account più sicuro richiedendoti di verificare il tuo login usando un altro dispositivo come una chiave di sicurezza, app di autenticazione, SMS, telefonata, o email. Può essere abilitata nella cassaforte web su bitwarden.com. Vuoi visitare il sito?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Errore durante il salvataggio delle credenziali. Controlla la console per più dettagli.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Successo" + }, + "removePasskey": { + "message": "Rimuovi passkey" + }, + "passkeyRemoved": { + "message": "Passkey rimossa" + }, + "unassignedItemsBannerNotice": { + "message": "Avviso: gli elementi dell'organizzazione non assegnati non sono più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e sono ora accessibili solo tramite la Console di amministrazione." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Avviso: dal 16 maggio 2024, gli elementi dell'organizzazione non assegnati non saranno più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e saranno accessibili solo tramite la Console di amministrazione." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assegna questi elementi ad una raccolta dalla", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "per renderli visibili.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Console di amministrazione" + }, + "errorAssigningTargetCollection": { + "message": "Errore nell'assegnazione della raccolta di destinazione." + }, + "errorAssigningTargetFolder": { + "message": "Errore nell'assegnazione della cartella di destinazione." } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 990775c084..52ff21727a 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - 無料パスワードマネージャー", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden はあらゆる端末で使える、安全な無料パスワードマネージャーです。", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "マスターパスワードの変更" }, + "continueToWebApp": { + "message": "ウェブアプリに進みますか?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Bitwarden ウェブアプリでマスターパスワードを変更できます。" + }, "fingerprintPhrase": { "message": "パスフレーズ", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "フォルダを追加しました" }, - "changeMasterPass": { - "message": "マスターパスワードの変更" - }, - "changeMasterPasswordConfirmation": { - "message": "マスターパスワードは bitwarden.com ウェブ保管庫で変更できます。ウェブサイトを開きますか?" - }, "twoStepLoginConfirmation": { "message": "2段階認証を使うと、ログイン時にセキュリティキーや認証アプリ、SMS、電話やメールでの認証を必要にすることでアカウントをさらに安全に出来ます。2段階認証は bitwarden.com ウェブ保管庫で有効化できます。ウェブサイトを開きますか?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "資格情報の保存中にエラーが発生しました。詳細はコンソールを確認してください。", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "成功" + }, + "removePasskey": { + "message": "パスキーを削除" + }, + "passkeyRemoved": { + "message": "パスキーを削除しました" + }, + "unassignedItemsBannerNotice": { + "message": "注意: 割り当てられていない組織アイテムは、すべての保管庫ビューでは表示されなくなり、管理コンソールからのみアクセスできるようになります。" + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "お知らせ:2024年5月16日に、 割り当てられていない組織アイテムは、すべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。" + }, + "unassignedItemsBannerCTAPartOne": { + "message": "これらのアイテムのコレクションへの割り当てを", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "で実行すると表示できるようになります。", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "管理コンソール" + }, + "errorAssigningTargetCollection": { + "message": "ターゲットコレクションの割り当てに失敗しました。" + }, + "errorAssigningTargetFolder": { + "message": "ターゲットフォルダーの割り当てに失敗しました。" } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index fdcd46bc3c..2c18502eca 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 4a37361f29..5d1b024c60 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 20fb2ea458..047270808e 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -3,11 +3,11 @@ "message": "ಬಿಟ್ವಾರ್ಡೆನ್" }, "extName": { - "message": "ಬಿಟ್‌ವಾರ್ಡೆನ್ - ಉಚಿತ ಪಾಸ್‌ವರ್ಡ್ ನಿರ್ವಾಹಕ", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "ನಿಮ್ಮ ಎಲ್ಲಾ ಸಾಧನಗಳಿಗೆ ಸುರಕ್ಷಿತ ಮತ್ತು ಉಚಿತ ಪಾಸ್‌ವರ್ಡ್ ನಿರ್ವಾಹಕ.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "ಮಾಸ್ಟರ್ ಪಾಸ್ವರ್ಡ್ ಬದಲಾಯಿಸಿ" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "ಫಿಂಗರ್ಪ್ರಿಂಟ್ ಫ್ರೇಸ್", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "ಫೋಲ್ಡರ್ ಸೇರಿಸಿ" }, - "changeMasterPass": { - "message": "ಮಾಸ್ಟರ್ ಪಾಸ್ವರ್ಡ್ ಬದಲಾಯಿಸಿ" - }, - "changeMasterPasswordConfirmation": { - "message": "ನಿಮ್ಮ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ನೀವು bitwarden.com ವೆಬ್ ವಾಲ್ಟ್‌ನಲ್ಲಿ ಬದಲಾಯಿಸಬಹುದು. ನೀವು ಈಗ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಭೇಟಿ ನೀಡಲು ಬಯಸುವಿರಾ?" - }, "twoStepLoginConfirmation": { "message": "ಭದ್ರತಾ ಕೀ, ದೃಢೀಕರಣ ಅಪ್ಲಿಕೇಶನ್, ಎಸ್‌ಎಂಎಸ್, ಫೋನ್ ಕರೆ ಅಥವಾ ಇಮೇಲ್‌ನಂತಹ ಮತ್ತೊಂದು ಸಾಧನದೊಂದಿಗೆ ನಿಮ್ಮ ಲಾಗಿನ್ ಅನ್ನು ಪರಿಶೀಲಿಸುವ ಅಗತ್ಯವಿರುವ ಎರಡು ಹಂತದ ಲಾಗಿನ್ ನಿಮ್ಮ ಖಾತೆಯನ್ನು ಹೆಚ್ಚು ಸುರಕ್ಷಿತಗೊಳಿಸುತ್ತದೆ. ಬಿಟ್ವಾರ್ಡೆನ್.ಕಾಮ್ ವೆಬ್ ವಾಲ್ಟ್ನಲ್ಲಿ ಎರಡು-ಹಂತದ ಲಾಗಿನ್ ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಬಹುದು. ನೀವು ಈಗ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಭೇಟಿ ನೀಡಲು ಬಯಸುವಿರಾ?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 394534d6b0..4bc4302f8b 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - 무료 비밀번호 관리자", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "당신의 모든 기기에서 사용할 수 있는, 안전한 무료 비밀번호 관리자입니다.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "마스터 비밀번호 변경" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "지문 구절", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "폴더 추가함" }, - "changeMasterPass": { - "message": "마스터 비밀번호 변경" - }, - "changeMasterPasswordConfirmation": { - "message": "bitwarden.com 웹 보관함에서 마스터 비밀번호를 바꿀 수 있습니다. 지금 웹 사이트를 방문하시겠습니까?" - }, "twoStepLoginConfirmation": { "message": "2단계 인증은 보안 키, 인증 앱, SMS, 전화 통화 등의 다른 기기로 사용자의 로그인 시도를 검증하여 사용자의 계정을 더욱 안전하게 만듭니다. 2단계 인증은 bitwarden.com 웹 보관함에서 활성화할 수 있습니다. 지금 웹 사이트를 방문하시겠습니까?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index fe73a8a28a..b1a2c857e0 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Saugi ir nemokama slaptažodžių tvarkyklė visiems įrenginiams.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Keisti pagrindinį slaptažodį" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Pirštų atspaudų frazė", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Katalogas pridėtas" }, - "changeMasterPass": { - "message": "Keisti pagrindinį slaptažodį" - }, - "changeMasterPasswordConfirmation": { - "message": "Pagrindinį slaptažodį galite pakeisti „bitwarden.com“ žiniatinklio saugykloje. Ar norite dabar apsilankyti svetainėje?" - }, "twoStepLoginConfirmation": { "message": "Prisijungus dviem veiksmais, jūsų paskyra tampa saugesnė, reikalaujant patvirtinti prisijungimą naudojant kitą įrenginį, pvz., saugos raktą, autentifikavimo programėlę, SMS, telefono skambutį ar el. paštą. Dviejų žingsnių prisijungimą galima įjungti „bitwarden.com“ interneto saugykloje. Ar norite dabar apsilankyti svetainėje?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Klaida išsaugant kredencialus. Išsamesnės informacijos patikrink konsolėje.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Pašalinti slaptaraktį" + }, + "passkeyRemoved": { + "message": "Pašalintas slaptaraktis" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 83f3de2556..4055693486 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Drošs bezmaksas paroļu pārvaldnieks visām Tavām ierīcēm.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Mainīt galveno paroli" }, + "continueToWebApp": { + "message": "Pāriet uz tīmekļa lietotni?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Savu galveno paroli var mainīt Bitwarden tīmekļa lietotnē." + }, "fingerprintPhrase": { "message": "Atpazīšanas vārdkopa", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Pievienoja mapi" }, - "changeMasterPass": { - "message": "Mainīt galveno paroli" - }, - "changeMasterPasswordConfirmation": { - "message": "Galveno paroli ir iespējams mainīt bitwarden.com tīmekļa glabātavā. Vai tagad apmeklēt tīmekļvietni?" - }, "twoStepLoginConfirmation": { "message": "Divpakāpju pieteikšanās padara kontu krietni drošāku, pieprasot apstiprināt pieteikšanos ar tādu citu ierīču vai pakalpojumu starpniecību kā drošības atslēga, autentificētāja lietotne, īsziņa, tālruņa zvans vai e-pasts. Divpakāpju pieteikšanos var iespējot bitwarden.com tīmekļa glabātavā. Vai tagad apmeklēt tīmekļvietni?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Kļūda piekļuves informācijas saglabāšanā. Jāpārbauda, vai konsolē ir izvērstāka informācija.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Izdevās" + }, + "removePasskey": { + "message": "Noņemt piekļuves atslēgu" + }, + "passkeyRemoved": { + "message": "Piekļuves atslēga noņemta" + }, + "unassignedItemsBannerNotice": { + "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" un ir pieejami tikai pārvaldības konsolē." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Jāņem vērā: no 2024. gada 16. maija nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" un būs pieejami tikai pārvaldības konsolē." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Piešķirt šos vienumus krājumam", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "lai padarītu tos redzamus.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "pārvaldības konsolē," + }, + "errorAssigningTargetCollection": { + "message": "Kļūda mērķa krājuma piešķiršanā." + }, + "errorAssigningTargetFolder": { + "message": "Kļūda mērķa mapes piešķiršanā." } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index c2d1006694..d9703137fe 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - സൗജന്യ പാസ്സ്‌വേഡ് മാനേജർ ", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "നിങ്ങളുടെ എല്ലാ ഉപകരണങ്ങൾക്കും സുരക്ഷിതവും സൗജന്യവുമായ പാസ്‌വേഡ് മാനേജർ.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "പ്രാഥമിക പാസ്‌വേഡ് മാറ്റുക" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "ഫിംഗർപ്രിന്റ് ഫ്രേസ്‌", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "ചേർക്കപ്പെട്ട ഫോൾഡർ" }, - "changeMasterPass": { - "message": "പ്രാഥമിക പാസ്‌വേഡ് മാറ്റുക" - }, - "changeMasterPasswordConfirmation": { - "message": "തങ്ങൾക്കു ബിറ്റ് വാർഡൻ വെബ് വാൾട്ടിൽ പ്രാഥമിക പാസ്‌വേഡ് മാറ്റാൻ സാധിക്കും.വെബ്സൈറ്റ് ഇപ്പോൾ സന്ദർശിക്കാൻ ആഗ്രഹിക്കുന്നുവോ?" - }, "twoStepLoginConfirmation": { "message": "സുരക്ഷാ കീ, ഓതന്റിക്കേറ്റർ അപ്ലിക്കേഷൻ, SMS, ഫോൺ കോൾ അല്ലെങ്കിൽ ഇമെയിൽ പോലുള്ള മറ്റൊരു ഉപകരണം ഉപയോഗിച്ച് തങ്ങളുടെ ലോഗിൻ സ്ഥിരീകരിക്കാൻ ആവശ്യപ്പെടുന്നതിലൂടെ രണ്ട്-ഘട്ട ലോഗിൻ തങ്ങളുടെ അക്കൗണ്ടിനെ കൂടുതൽ സുരക്ഷിതമാക്കുന്നു. bitwarden.com വെബ് വാൾട്ടിൽ രണ്ട്-ഘട്ട ലോഗിൻ പ്രവർത്തനക്ഷമമാക്കാനാകും.തങ്ങള്ക്കു ഇപ്പോൾ വെബ്സൈറ്റ് സന്ദർശിക്കാൻ ആഗ്രഹമുണ്ടോ?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 7ddbe00732..f67f617d3b 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - विनामूल्य पासवर्ड व्यवस्थापक", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "तुमच्या सर्व उपकरणांसाठी एक सुरक्षित व विनामूल्य पासवर्ड व्यवस्थापक.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "मुख्य पासवर्ड बदला" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "अंगुलिमुद्रा वाक्यांश", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 4a37361f29..5d1b024c60 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index d7a8345a23..649163a8dc 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden — Fri passordbehandling", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden er en sikker og fri passordbehandler for alle dine PCer og mobiler.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Endre hovedpassordet" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingeravtrykksfrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "La til en mappe" }, - "changeMasterPass": { - "message": "Endre hovedpassordet" - }, - "changeMasterPasswordConfirmation": { - "message": "Du kan endre superpassordet ditt på bitwarden.net-netthvelvet. Vil du besøke det nettstedet nå?" - }, "twoStepLoginConfirmation": { "message": "2-trinnsinnlogging gjør kontoen din mer sikker, ved å kreve at du verifiserer din innlogging med en annen enhet, f.eks. en autentiseringsapp, SMS, e-post, telefonsamtale, eller sikkerhetsnøkkel. 2-trinnsinnlogging kan aktiveres i netthvelvet ditt på bitwarden.com. Vil du besøke bitwarden.com nå?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 4a37361f29..5d1b024c60 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index c28b99b7c2..5a52b4f7ef 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Gratis wachtwoordbeheer", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Een veilige en gratis oplossing voor wachtwoordbeheer voor al je apparaten.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Hoofdwachtwoord wijzigen" }, + "continueToWebApp": { + "message": "Doorgaan naar web-app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Je kunt je hoofdwachtwoord wijzigen in de Bitwarden-webapp." + }, "fingerprintPhrase": { "message": "Vingerafdrukzin", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Map is toegevoegd" }, - "changeMasterPass": { - "message": "Hoofdwachtwoord wijzigen" - }, - "changeMasterPasswordConfirmation": { - "message": "Je kunt je hoofdwachtwoord wijzigen in de kluis op bitwarden.com. Wil je de website nu bezoeken?" - }, "twoStepLoginConfirmation": { "message": "Tweestapsaanmelding beschermt je account door je inlogpoging te bevestigen met een ander apparaat zoals een beveiligingscode, authenticatie-app, SMS, spraakoproep of e-mail. Je kunt Tweestapsaanmelding inschakelen in de webkluis op bitwarden.com. Wil je de website nu bezoeken?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Succes" + }, + "removePasskey": { + "message": "Passkey verwijderen" + }, + "passkeyRemoved": { + "message": "Passkey verwijderd" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Fout bij toewijzen doelverzameling." + }, + "errorAssigningTargetFolder": { + "message": "Fout bij toewijzen doelmap." } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 4a37361f29..5d1b024c60 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 4a37361f29..5d1b024c60 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index e4c3b7b171..e768a70d52 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - darmowy menedżer haseł", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bezpieczny i darmowy menedżer haseł dla wszystkich Twoich urządzeń.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Zmień hasło główne" }, + "continueToWebApp": { + "message": "Kontynuować do aplikacji internetowej?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Możesz zmienić swoje hasło główne w aplikacji internetowej Bitwarden." + }, "fingerprintPhrase": { "message": "Unikalny identyfikator konta", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder został dodany" }, - "changeMasterPass": { - "message": "Zmień hasło główne" - }, - "changeMasterPasswordConfirmation": { - "message": "Hasło główne możesz zmienić na stronie sejfu bitwarden.com. Czy chcesz przejść do tej strony?" - }, "twoStepLoginConfirmation": { "message": "Logowanie dwustopniowe sprawia, że konto jest bardziej bezpieczne poprzez wymuszenie potwierdzenia logowania z innego urządzenia, takiego jak z klucza bezpieczeństwa, aplikacji uwierzytelniającej, wiadomości SMS, telefonu lub adresu e-mail. Logowanie dwustopniowe możesz włączyć w sejfie internetowym bitwarden.com. Czy chcesz przejść do tej strony?" }, @@ -2709,7 +2709,7 @@ "message": "Otwórz rozszerzenie w nowym oknie, aby dokończyć logowanie." }, "popoutExtension": { - "message": "Popout extension" + "message": "Otwórz rozszerzenie w nowym oknie" }, "launchDuo": { "message": "Uruchom DUO" @@ -2822,7 +2822,7 @@ "message": "Wybierz dane logowania do których przypisać passkey" }, "passkeyItem": { - "message": "Passkey Item" + "message": "Element Passkey" }, "overwritePasskey": { "message": "Zastąpić passkey?" @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Błąd podczas zapisywania danych logowania. Sprawdź konsolę, aby uzyskać szczegóły.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Sukces" + }, + "removePasskey": { + "message": "Usuń passkey" + }, + "passkeyRemoved": { + "message": "Passkey został usunięty" + }, + "unassignedItemsBannerNotice": { + "message": "Uwaga: Nieprzypisane elementy organizacji nie są już widoczne w widoku Wszystkie sejfy i są teraz dostępne tylko przez Konsolę Administracyjną." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Uwaga: 16 maja 2024 r. nieprzypisana elementy w organizacji nie będą już widoczne w widoku Wszystkie sejfy i będą dostępne tylko przez Konsolę Administracyjną." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Przypisz te elementy do kolekcji z", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": ", aby były widoczne.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Konsola Administracyjna" + }, + "errorAssigningTargetCollection": { + "message": "Wystąpił błąd podczas przypisywania kolekcji." + }, + "errorAssigningTargetFolder": { + "message": "Wystąpił błąd podczas przypisywania folderu." } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 3445a3ff5f..c6e62fbd4f 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Um gerenciador de senhas seguro e gratuito para todos os seus dispositivos.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Alterar Senha Mestra" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Frase Biométrica", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Pasta adicionada" }, - "changeMasterPass": { - "message": "Alterar Senha Mestra" - }, - "changeMasterPasswordConfirmation": { - "message": "Você pode alterar a sua senha mestra no cofre web em bitwarden.com. Você deseja visitar o site agora?" - }, "twoStepLoginConfirmation": { "message": "O login de duas etapas torna a sua conta mais segura ao exigir que digite um código de segurança de um aplicativo de autenticação quando for iniciar a sessão. O login de duas etapas pode ser ativado no cofre web bitwarden.com. Deseja visitar o site agora?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 117c5be6b4..06ba8eed26 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Gestor de Palavras-passe", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Um gestor de palavras-passe seguro e gratuito para todos os seus dispositivos.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Alterar palavra-passe mestra" }, + "continueToWebApp": { + "message": "Continuar para a aplicação Web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Pode alterar a sua palavra-passe mestra na aplicação Web Bitwarden." + }, "fingerprintPhrase": { "message": "Frase de impressão digital", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Pasta adicionada" }, - "changeMasterPass": { - "message": "Alterar palavra-passe mestra" - }, - "changeMasterPasswordConfirmation": { - "message": "Pode alterar o seu endereço de e-mail no cofre do site bitwarden.com. Deseja visitar o site agora?" - }, "twoStepLoginConfirmation": { "message": "A verificação de dois passos torna a sua conta mais segura, exigindo que verifique o seu início de sessão com outro dispositivo, como uma chave de segurança, aplicação de autenticação, SMS, chamada telefónica ou e-mail. A verificação de dois passos pode ser configurada em bitwarden.com. Pretende visitar o site agora?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Erro ao guardar as credenciais. Verifique a consola para obter detalhes.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Com sucesso" + }, + "removePasskey": { + "message": "Remover chave de acesso" + }, + "passkeyRemoved": { + "message": "Chave de acesso removida" + }, + "unassignedItemsBannerNotice": { + "message": "Aviso: Os itens da organização não atribuídos já não são visíveis na vista Todos os cofres e só são acessíveis através da Consola de administração." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Aviso: A 16 de maio de 2024, os itens da organização não atribuídos deixarão de estar visíveis na vista Todos os cofres e só estarão acessíveis através da consola de administração." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Atribua estes itens a uma coleção a partir da", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "para os tornar visíveis.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Consola de administração" + }, + "errorAssigningTargetCollection": { + "message": "Erro ao atribuir a coleção de destino." + }, + "errorAssigningTargetFolder": { + "message": "Erro ao atribuir a pasta de destino." } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 4851add018..b3e0a2066f 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Manager de parole gratuit", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Un manager de parole sigur și gratuit pentru toate dispozitivele dvs.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Schimbare parolă principală" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fraza amprentă", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Dosar adăugat" }, - "changeMasterPass": { - "message": "Schimbare parolă principală" - }, - "changeMasterPasswordConfirmation": { - "message": "Puteți modifica parola principală în seiful web bitwarden.com. Doriți să vizitați saitul acum?" - }, "twoStepLoginConfirmation": { "message": "Autentificarea în două etape vă face contul mai sigur, prin solicitarea unei verificări de autentificare cu un alt dispozitiv, cum ar fi o cheie de securitate, o aplicație de autentificare, un SMS, un apel telefonic sau un e-mail. Autentificarea în două etape poate fi configurată în seiful web bitwarden.com. Doriți să vizitați site-ul web acum?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index fb4e2abaac..e594dbdce2 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - бесплатный менеджер паролей", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Защищенный и бесплатный менеджер паролей для всех ваших устройств.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Изменить мастер-пароль" }, + "continueToWebApp": { + "message": "Перейти к веб-приложению?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Изменить мастер-пароль можно в веб-приложении Bitwarden." + }, "fingerprintPhrase": { "message": "Фраза отпечатка", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Папка добавлена" }, - "changeMasterPass": { - "message": "Изменить мастер-пароль" - }, - "changeMasterPasswordConfirmation": { - "message": "Вы можете изменить свой мастер-пароль на bitwarden.com. Перейти на сайт сейчас?" - }, "twoStepLoginConfirmation": { "message": "Двухэтапная аутентификация делает аккаунт более защищенным, поскольку требуется подтверждение входа при помощи другого устройства, например, ключа безопасности, приложения-аутентификатора, SMS, телефонного звонка или электронной почты. Двухэтапная аутентификация включается на bitwarden.com. Перейти на сайт сейчас?" }, @@ -688,10 +688,10 @@ "message": "Запрос на обновление пароля логина при обнаружении изменений на сайте. Применяется ко всем авторизованным аккаунтам." }, "enableUsePasskeys": { - "message": "Запрос на сохранение и использование ключей доступа" + "message": "Запрос на сохранение и использование passkey" }, "usePasskeysDesc": { - "message": "Запрос на сохранение новых ключей или в авторизация с ключами, хранящимися в вашем хранилище. Применяется ко всем авторизованным аккаунтам." + "message": "Запрос на сохранение новых passkey или в авторизация с passkey, хранящимися в вашем хранилище. Применяется ко всем авторизованным аккаунтам." }, "notificationChangeDesc": { "message": "Обновить этот пароль в Bitwarden?" @@ -2786,25 +2786,25 @@ "message": "Подтвердите пароль к файлу" }, "typePasskey": { - "message": "Ключ доступа" + "message": "Passkey" }, "passkeyNotCopied": { - "message": "Ключ доступа не будет скопирован" + "message": "Passkey не будет скопирован" }, "passkeyNotCopiedAlert": { - "message": "Ключ доступа не будет скопирован в клонированный элемент. Продолжить клонирование этого элемента?" + "message": "Passkey не будет скопирован в клонированный элемент. Продолжить клонирование этого элемента?" }, "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Необходима верификация со стороны инициирующего сайта. Для аккаунтов без мастер-пароля эта возможность пока не реализована." }, "logInWithPasskey": { - "message": "Войти с ключом доступа?" + "message": "Войти с passkey?" }, "passkeyAlreadyExists": { - "message": "Для данного приложения уже существует ключ доступа." + "message": "Для данного приложения уже существует passkey." }, "noPasskeysFoundForThisApplication": { - "message": "Для данного приложения ключей доступа не найдено." + "message": "Для данного приложения не найден passkey." }, "noMatchingPasskeyLogin": { "message": "У вас нет подходящего логина для этого сайта." @@ -2813,28 +2813,28 @@ "message": "Подтвердить" }, "savePasskey": { - "message": "Сохранить ключ доступа" + "message": "Сохранить passkey" }, "savePasskeyNewLogin": { - "message": "Сохранить ключ доступа как новый логин" + "message": "Сохранить passkey как новый логин" }, "choosePasskey": { - "message": "Выберите логин, для которого будет сохранен данный ключ доступа" + "message": "Выберите логин, для которого будет сохранен данный passkey" }, "passkeyItem": { - "message": "Ключ доступа элемента" + "message": "Элемент passkey" }, "overwritePasskey": { - "message": "Перезаписать ключ доступа?" + "message": "Перезаписать passkey?" }, "overwritePasskeyAlert": { - "message": "Этот элемент уже содержит ключ доступа. Вы уверены, что хотите перезаписать текущий ключ?" + "message": "Этот элемент уже содержит passkey. Вы уверены, что хотите перезаписать текущий passkey?" }, "featureNotSupported": { "message": "Функция пока не поддерживается" }, "yourPasskeyIsLocked": { - "message": "Для использования ключа доступа необходима аутентификация. Для продолжения работы подтвердите свою личность." + "message": "Для использования passkey необходима аутентификация. Для продолжения работы подтвердите свою личность." }, "multifactorAuthenticationCancelled": { "message": "Многофакторная аутентификация отменена" @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Ошибка сохранения учетных данных. Проверьте консоль для получения подробной информации.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Успешно" + }, + "removePasskey": { + "message": "Удалить passkey" + }, + "passkeyRemoved": { + "message": "Passkey удален" + }, + "unassignedItemsBannerNotice": { + "message": "Уведомление: Неприсвоенные элементы организации больше не видны в представлении \"Все хранилища\" и доступны только через консоль администратора." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Уведомление: с 16 мая 2024 года не назначенные элементы организации больше не будут видны в представлении \"Все хранилища\" и будут доступны только через консоль администратора." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Назначьте эти элементы в коллекцию из", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "чтобы сделать их видимыми.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "консоли администратора" + }, + "errorAssigningTargetCollection": { + "message": "Ошибка при назначении целевой коллекции." + }, + "errorAssigningTargetFolder": { + "message": "Ошибка при назначении целевой папки." } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index ca466ef7bb..05e2dc3edd 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -3,11 +3,11 @@ "message": "බිට්වාඩන්" }, "extName": { - "message": "බිට්වාඩන් - නොමිලේ මුරපදය කළමනාකරු", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "ඔබගේ සියලු උපාංග සඳහා ආරක්ෂිත සහ නොමිලේ මුරපද කළමණාකරුවෙකු.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "ප්රධාන මුරපදය වෙනස්" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "ඇඟිලි සලකුණු වාක්ය ඛණ්ඩය", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "එකතු කරන ලද ෆෝල්ඩරය" }, - "changeMasterPass": { - "message": "ප්රධාන මුරපදය වෙනස්" - }, - "changeMasterPasswordConfirmation": { - "message": "bitwarden.com වෙබ් සුරක්ෂිතාගාරයේ ඔබේ ප්රධාන මුරපදය වෙනස් කළ හැකිය. ඔබට දැන් වෙබ් අඩවියට පිවිසීමට අවශ්යද?" - }, "twoStepLoginConfirmation": { "message": "ආරක්ෂක යතුරක්, සත්යාපන යෙදුම, කෙටි පණිවුඩ, දුරකථන ඇමතුමක් හෝ විද්යුත් තැපෑල වැනි වෙනත් උපාංගයක් සමඟ ඔබේ පිවිසුම සත්යාපනය කිරීමට ඔබට අවශ්ය වීමෙන් ද්වි-පියවර පිවිසුම ඔබගේ ගිණුම වඩාත් සුරක්ෂිත කරයි. බිට්වොන්.com වෙබ් සුරක්ෂිතාගාරයේ ද්වි-පියවර පිවිසුම සක්රීය කළ හැකිය. ඔබට දැන් වෙබ් අඩවියට පිවිසීමට අවශ්යද?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 382dc82245..eab1d105eb 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Bezplatný správca hesiel", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "bitwarden je bezpečný a bezplatný správca hesiel pre všetky vaše zariadenia.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Zmeniť hlavné heslo" }, + "continueToWebApp": { + "message": "Pokračovať vo webovej aplikácii?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hlavné heslo si môžete zmeniť vo webovej aplikácii Bitwarden." + }, "fingerprintPhrase": { "message": "Fráza odtlačku", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Pridaný priečinok" }, - "changeMasterPass": { - "message": "Zmeniť hlavné heslo" - }, - "changeMasterPasswordConfirmation": { - "message": "Teraz si môžete zmeniť svoje hlavné heslo vo webovom trezore bitwarden.com. Chcete navštíviť túto stránku teraz?" - }, "twoStepLoginConfirmation": { "message": "Dvojstupňové prihlasovanie robí váš účet bezpečnejším vďaka vyžadovaniu bezpečnostného kódu z overovacej aplikácie vždy, keď sa prihlásite. Dvojstupňové prihlasovanie môžete povoliť vo webovom trezore bitwarden.com. Chcete navštíviť túto stránku teraz?" }, @@ -1754,7 +1754,7 @@ } }, "send": { - "message": "Odoslať", + "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "searchSends": { @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Chyba pri ukladaní prihlasovacích údajov. Viac informácii nájdete v konzole.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Úspech" + }, + "removePasskey": { + "message": "Odstrániť prístupový kľúč" + }, + "passkeyRemoved": { + "message": "Prístupový kľúč bol odstránený" + }, + "unassignedItemsBannerNotice": { + "message": "Upozornenie: Nepriradené položky organizácie už nie sú viditeľné v zobrazení Všetky trezory a sú prístupné iba cez Správcovskú konzolu." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Upozornenie: 16. mája 2024 nepriradené položky organizácie už nebudú viditeľné v zobrazení Všetky trezory a budú prístupné iba cez Správcovskú konzolu." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Priradiť tieto položky do zbierky zo", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": ", aby boli viditeľné.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Správcovská konzola" + }, + "errorAssigningTargetCollection": { + "message": "Chyba pri priraďovaní cieľovej kolekcie." + }, + "errorAssigningTargetFolder": { + "message": "Chyba pri priraďovaní cieľového priečinka." } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index e11a56acde..935678efc8 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Brezplačni upravitelj gesel", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Varen in brezplačen upravitelj gesel za vse vaše naprave.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Spremeni glavno geslo" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Identifikacijsko geslo", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Mapa dodana" }, - "changeMasterPass": { - "message": "Spremeni glavno geslo" - }, - "changeMasterPasswordConfirmation": { - "message": "Svoje glavno geslo lahko spremenite v Bitwardnovem spletnem trezorju. Želite zdaj obiskati Bitwardnovo spletno stran?" - }, "twoStepLoginConfirmation": { "message": "Avtentikacija v dveh korakih dodatno varuje vaš račun, saj zahteva, da vsakokratno prijavo potrdite z drugo napravo, kot je varnostni ključ, aplikacija za preverjanje pristnosti, SMS, telefonski klic ali e-pošta. Avtentikacijo v dveh korakih lahko omogočite v spletnem trezorju bitwarden.com. Ali želite spletno stran obiskati sedaj?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index d598263e91..5819546800 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - бесплатни менаџер лозинки", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Сигурни и бесплатни менаџер лозинки за све ваше уређаје.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Промени главну лозинку" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Сигурносна Фраза Сефа", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Фасцикла додата" }, - "changeMasterPass": { - "message": "Промени главну лозинку" - }, - "changeMasterPasswordConfirmation": { - "message": "Можете променити главну лозинку у Вашем сефу на bitwarden.com. Да ли желите да посетите веб страницу сада?" - }, "twoStepLoginConfirmation": { "message": "Пријава у два корака чини ваш налог сигурнијим захтевом да верификујете своје податке помоћу другог уређаја, као што су безбедносни кључ, апликација, СМС-а, телефонски позив или имејл. Пријављивање у два корака може се омогућити на веб сефу. Да ли желите да посетите веб страницу сада?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Грешка при чувању акредитива. Проверите конзолу за детаље.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Уклонити приступачни кључ" + }, + "passkeyRemoved": { + "message": "Приступачни кључ је уклоњен" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 082fbac350..d96e86b8d3 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Gratis lösenordshanterare", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden är en säker och gratis lösenordshanterare för alla dina enheter.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Ändra huvudlösenord" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingeravtrycksfras", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Lade till mapp" }, - "changeMasterPass": { - "message": "Ändra huvudlösenord" - }, - "changeMasterPasswordConfirmation": { - "message": "Du kan ändra ditt huvudlösenord på bitwardens webbvalv. Vill du besöka webbplatsen nu?" - }, "twoStepLoginConfirmation": { "message": "Tvåstegsverifiering gör ditt konto säkrare genom att kräva att du verifierar din inloggning med en annan enhet, t.ex. en säkerhetsnyckel, autentiseringsapp, SMS, telefonsamtal eller e-post. Tvåstegsverifiering kan aktiveras i Bitwardens webbvalv. Vill du besöka webbplatsen nu?" }, @@ -2931,7 +2931,7 @@ "message": "active" }, "locked": { - "message": "locked" + "message": "låst" }, "unlocked": { "message": "unlocked" @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 4a37361f29..5d1b024c60 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 011b7983d4..794d0e6c22 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -3,11 +3,11 @@ "message": "bitwarden" }, "extName": { - "message": "bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "bitwarden is a secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change Master Password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint Phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "เพิ่มโฟลเดอร์แล้ว" }, - "changeMasterPass": { - "message": "Change Master Password" - }, - "changeMasterPasswordConfirmation": { - "message": "คุณสามารถเปลี่ยนรหัสผ่านหลักได้ที่เว็บตู้เซฟ bitwarden.com คุณต้องการเปิดเว็บไซต์เลยหรือไม่?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to enter a security code from an authenticator app whenever you log in. Two-step login can be enabled on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 391fef8ddc..8408253b86 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Ücretsiz Parola Yöneticisi", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Tüm cihazlarınız için güvenli ve ücretsiz bir parola yöneticisi.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Ana parolayı değiştir" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Parmak izi ifadesi", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Klasör eklendi" }, - "changeMasterPass": { - "message": "Ana parolayı değiştir" - }, - "changeMasterPasswordConfirmation": { - "message": "Ana parolanızı bitwarden.com web kasası üzerinden değiştirebilirsiniz. Siteye gitmek ister misiniz?" - }, "twoStepLoginConfirmation": { "message": "İki aşamalı giriş, hesabınıza girererken işlemi bir güvenlik anahtarı, şifrematik uygulaması, SMS, telefon araması veya e-posta gibi ek bir yöntemle doğrulamanızı isteyerek hesabınızın güvenliğini artırır. İki aşamalı giriş özelliğini bitwarden.com web kasası üzerinden etkinleştirebilirsiniz. Şimdi siteye gitmek ister misiniz?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Kimlik bilgileri kaydedilirken hata oluştu. Ayrıntılar için konsolu kontrol edin.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 98dabb597d..b590b92041 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden - це захищений і безкоштовний менеджер паролів для всіх ваших пристроїв.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Змінити головний пароль" }, + "continueToWebApp": { + "message": "Продовжити у вебпрограмі?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Ви можете змінити головний пароль у вебпрограмі Bitwarden." + }, "fingerprintPhrase": { "message": "Фраза відбитка", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Теку додано" }, - "changeMasterPass": { - "message": "Змінити головний пароль" - }, - "changeMasterPasswordConfirmation": { - "message": "Ви можете змінити головний пароль в сховищі на bitwarden.com. Хочете перейти на вебсайт зараз?" - }, "twoStepLoginConfirmation": { "message": "Двоетапна перевірка дає змогу надійніше захистити ваш обліковий запис, вимагаючи підтвердження входу з використанням іншого пристрою, наприклад, за допомогою ключа безпеки, програми автентифікації, SMS, телефонного виклику, або е-пошти. Ви можете налаштувати двоетапну перевірку в сховищі на bitwarden.com. Хочете перейти на вебсайт зараз?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Помилка збереження облікових даних. Перегляньте подробиці в консолі.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Успішно" + }, + "removePasskey": { + "message": "Вилучити ключ доступу" + }, + "passkeyRemoved": { + "message": "Ключ доступу вилучено" + }, + "unassignedItemsBannerNotice": { + "message": "Примітка: непризначені елементи організації більше не видимі у поданні \"Усі сховища\" і доступні лише в консолі адміністратора." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Примітка: 16 травня 2024 року непризначені елементи організації більше не будуть видимі у поданні \"Усі сховища\" і будуть доступні лише через консоль адміністратора." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Призначте ці елементи збірці в", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "щоб зробити їх видимими.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "консолі адміністратора," + }, + "errorAssigningTargetCollection": { + "message": "Помилка призначення цільової збірки." + }, + "errorAssigningTargetFolder": { + "message": "Помилка призначення цільової теки." } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 3a7cf5a794..ab1d0d515b 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Quản lý mật khẩu miễn phí", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Trình quản lý mật khẩu an toàn và miễn phí cho mọi thiết bị của bạn.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Thay đổi mật khẩu chính" }, + "continueToWebApp": { + "message": "Tiếp tục tới ứng dụng web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Bạn có thể thay đổi mật khẩu chính của mình trên Bitwarden bản web." + }, "fingerprintPhrase": { "message": "Fingerprint Phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -415,7 +421,7 @@ "message": "Khóa ngay" }, "lockAll": { - "message": "Lock all" + "message": "Khóa tất cả" }, "immediately": { "message": "Ngay lập tức" @@ -494,10 +500,10 @@ "message": "Tài khoản mới của bạn đã được tạo! Bạn có thể đăng nhập từ bây giờ." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Bạn đã đăng nhập thành công" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Bạn có thể đóng cửa sổ này" }, "masterPassSent": { "message": "Chúng tôi đã gửi cho bạn email có chứa gợi ý mật khẩu chính của bạn." @@ -522,16 +528,16 @@ "message": "Không thể tự động điền mục đã chọn trên trang này. Hãy thực hiện sao chép và dán thông tin một cách thủ công." }, "totpCaptureError": { - "message": "Unable to scan QR code from the current webpage" + "message": "Không thể quét mã QR từ trang web hiện tại" }, "totpCaptureSuccess": { - "message": "Authenticator key added" + "message": "Đã thêm khóa xác thực" }, "totpCapture": { - "message": "Scan authenticator QR code from current webpage" + "message": "Quét mã QR xác thực từ trang web hiện tại" }, "copyTOTP": { - "message": "Copy Authenticator key (TOTP)" + "message": "Sao chép khóa Authenticator (TOTP)" }, "loggedOut": { "message": "Đã đăng xuất" @@ -557,12 +563,6 @@ "addedFolder": { "message": "Đã thêm thư mục" }, - "changeMasterPass": { - "message": "Thay đổi mật khẩu chính" - }, - "changeMasterPasswordConfirmation": { - "message": "Bạn có thể thay đổi mật khẩu chính trong trang web kho lưu trữ của Bitwarden. Bạn có muốn truy cập trang web ngay bây giờ không?" - }, "twoStepLoginConfirmation": { "message": "Xác thực hai lớp giúp cho tài khoản của bạn an toàn hơn bằng cách yêu cầu bạn xác minh thông tin đăng nhập của bạn bằng một thiết bị khác như khóa bảo mật, ứng dụng xác thực, SMS, cuộc gọi điện thoại hoặc email. Bạn có thể bật xác thực hai lớp trong kho bitwarden nền web. Bạn có muốn ghé thăm trang web bây giờ?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Lưu ý: Các mục tổ chức chưa được chỉ định sẽ không còn hiển thị trong chế độ xem Tất cả Vault và chỉ có thể truy cập được qua Bảng điều khiển dành cho quản trị viên." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Lưu ý: Vào ngày 16 tháng 5 năm 2024, các mục tổ chức chưa được chỉ định sẽ không còn hiển thị trong chế độ xem Tất cả Vault và sẽ chỉ có thể truy cập được qua Bảng điều khiển dành cho quản trị viên." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Gán các mục này vào một bộ sưu tập từ", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "để làm cho chúng hiển thị.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Bảng điều khiển dành cho quản trị viên" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 1c269640c8..a2f856c31b 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - 免费密码管理器", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "安全且免费的跨平台密码管理器。", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "更改主密码" }, + "continueToWebApp": { + "message": "前往网页 App 吗?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "指纹短语", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "文件夹已添加" }, - "changeMasterPass": { - "message": "修改主密码" - }, - "changeMasterPasswordConfirmation": { - "message": "您可以在 bitwarden.com 网页版密码库修改主密码。您现在要访问这个网站吗?" - }, "twoStepLoginConfirmation": { "message": "两步登录要求您从其他设备(例如安全钥匙、验证器 App、短信、电话或者电子邮件)来验证您的登录,这能使您的账户更加安全。两步登录需要在 bitwarden.com 网页版密码库中设置。现在访问此网站吗?" }, @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "保存凭据时出错。检查控制台以获取详细信息。", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "成功" + }, + "removePasskey": { + "message": "移除通行密钥" + }, + "passkeyRemoved": { + "message": "通行密钥已移除" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "管理控制台" + }, + "errorAssigningTargetCollection": { + "message": "分配目标集合时出错。" + }, + "errorAssigningTargetFolder": { + "message": "分配目标文件夹时出错。" } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index fdd6f4639b..1ecfdfc50e 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - 免費密碼管理工具", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden 是一款安全、免費、跨平台的密碼管理工具。", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "變更主密碼" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "指紋短語", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "資料夾已新增" }, - "changeMasterPass": { - "message": "變更主密碼" - }, - "changeMasterPasswordConfirmation": { - "message": "您可以在 bitwarden.com 網頁版密碼庫變更主密碼。現在要前往嗎?" - }, "twoStepLoginConfirmation": { "message": "兩步驟登入需要您從其他裝置(例如安全鑰匙、驗證器程式、SMS、手機或電子郵件)來驗證您的登入,這使您的帳戶更加安全。兩步驟登入可以在 bitwarden.com 網頁版密碼庫啟用。現在要前往嗎?" }, @@ -1058,7 +1058,7 @@ "message": "適用於所有已登入的帳戶。" }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "關閉你的瀏覽器內建密碼管理器設定以避免衝突。" }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "編輯瀏覽器設定" @@ -1168,7 +1168,7 @@ "message": "在每個登入資料旁顯示一個可辨識的圖片。" }, "faviconDescAlt": { - "message": "Show a recognizable image next to each login. Applies to all logged in accounts." + "message": "在每次登入時旁邊顯示可識別的圖片。適用於所有已登入的帳號。" }, "enableBadgeCounter": { "message": "顯示圖示計數器" @@ -2314,7 +2314,7 @@ "message": "如何自動填入" }, "autofillSelectInfoWithCommand": { - "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", + "message": "從此畫面中選擇一個項目;使用捷徑 $COMMAND$,或在設定中探索其他選項。", "placeholders": { "command": { "content": "$1", @@ -2323,7 +2323,7 @@ } }, "autofillSelectInfoWithoutCommand": { - "message": "Select an item from this screen, or explore other options in settings." + "message": "從此畫面中選擇一個項目,或在設定中探索其他選項。" }, "gotIt": { "message": "我知道了" @@ -2524,15 +2524,15 @@ "description": "Toggling an expand/collapse state." }, "filelessImport": { - "message": "Import your data to Bitwarden?", + "message": "匯入你的資料至 Bitwarden?", "description": "Default notification title for triggering a fileless import." }, "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", + "message": "保護你的 LastPass 資料並匯入至 Bitwarden?", "description": "LastPass specific notification title for triggering a fileless import." }, "lpCancelFilelessImport": { - "message": "Save as unencrypted file", + "message": "儲存為未加密的檔案", "description": "LastPass specific notification button text for cancelling a fileless import." }, "startFilelessImport": { @@ -2548,7 +2548,7 @@ "description": "Notification message for when an import has completed successfully." }, "dataImportFailed": { - "message": "Error importing. Check console for details.", + "message": "匯入時發生錯誤。檢查控制台以了解詳細資訊。", "description": "Notification message for when an import has failed." }, "importNetworkError": { @@ -2655,7 +2655,7 @@ "message": "再試一次" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "此操作需要驗證。設定 PIN 碼以繼續。" }, "setPin": { "message": "設定 PIN 碼" @@ -2667,7 +2667,7 @@ "message": "正在等待確認" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "無法完成生物辨識。" }, "needADifferentMethod": { "message": "需要不同的方法嗎?" @@ -2682,7 +2682,7 @@ "message": "用生物識別" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "輸入傳送到你的電子郵件的驗證碼。" }, "resendCode": { "message": "重新傳送驗證碼" @@ -2700,7 +2700,7 @@ } }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "啟動 Duo 並依照步驟完成登入。" }, "duoRequiredForAccount": { "message": "Duo two-step login is required for your account." @@ -2999,5 +2999,37 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "success": { + "message": "Success" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/auth/background/service-factories/auth-request-service.factory.ts b/apps/browser/src/auth/background/service-factories/auth-request-service.factory.ts index bd96a211ba..c18fd1a112 100644 --- a/apps/browser/src/auth/background/service-factories/auth-request-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/auth-request-service.factory.ts @@ -18,17 +18,25 @@ import { factory, } from "../../../platform/background/service-factories/factory-options"; import { - stateServiceFactory, - StateServiceInitOptions, -} from "../../../platform/background/service-factories/state-service.factory"; + stateProviderFactory, + StateProviderInitOptions, +} from "../../../platform/background/service-factories/state-provider.factory"; + +import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory"; +import { + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "./master-password-service.factory"; type AuthRequestServiceFactoryOptions = FactoryOptions; export type AuthRequestServiceInitOptions = AuthRequestServiceFactoryOptions & AppIdServiceInitOptions & + AccountServiceInitOptions & + MasterPasswordServiceInitOptions & CryptoServiceInitOptions & ApiServiceInitOptions & - StateServiceInitOptions; + StateProviderInitOptions; export function authRequestServiceFactory( cache: { authRequestService?: AuthRequestServiceAbstraction } & CachedServices, @@ -41,9 +49,11 @@ export function authRequestServiceFactory( async () => new AuthRequestService( await appIdServiceFactory(cache, opts), + await accountServiceFactory(cache, opts), + await internalMasterPasswordServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), await apiServiceFactory(cache, opts), - await stateServiceFactory(cache, opts), + await stateProviderFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/background/service-factories/key-connector-service.factory.ts b/apps/browser/src/auth/background/service-factories/key-connector-service.factory.ts index 4a0dd07b32..c602acadae 100644 --- a/apps/browser/src/auth/background/service-factories/key-connector-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/key-connector-service.factory.ts @@ -31,6 +31,11 @@ import { StateProviderInitOptions, } from "../../../platform/background/service-factories/state-provider.factory"; +import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory"; +import { + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "./master-password-service.factory"; import { TokenServiceInitOptions, tokenServiceFactory } from "./token-service.factory"; type KeyConnectorServiceFactoryOptions = FactoryOptions & { @@ -40,6 +45,8 @@ type KeyConnectorServiceFactoryOptions = FactoryOptions & { }; export type KeyConnectorServiceInitOptions = KeyConnectorServiceFactoryOptions & + AccountServiceInitOptions & + MasterPasswordServiceInitOptions & CryptoServiceInitOptions & ApiServiceInitOptions & TokenServiceInitOptions & @@ -58,6 +65,8 @@ export function keyConnectorServiceFactory( opts, async () => new KeyConnectorService( + await accountServiceFactory(cache, opts), + await internalMasterPasswordServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), await apiServiceFactory(cache, opts), await tokenServiceFactory(cache, opts), diff --git a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts index 2cc4692ca9..f184072cce 100644 --- a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts @@ -59,6 +59,7 @@ import { PasswordStrengthServiceInitOptions, } from "../../../tools/background/service_factories/password-strength-service.factory"; +import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory"; import { authRequestServiceFactory, AuthRequestServiceInitOptions, @@ -71,6 +72,10 @@ import { keyConnectorServiceFactory, KeyConnectorServiceInitOptions, } from "./key-connector-service.factory"; +import { + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "./master-password-service.factory"; import { tokenServiceFactory, TokenServiceInitOptions } from "./token-service.factory"; import { twoFactorServiceFactory, TwoFactorServiceInitOptions } from "./two-factor-service.factory"; import { @@ -81,6 +86,8 @@ import { type LoginStrategyServiceFactoryOptions = FactoryOptions; export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions & + AccountServiceInitOptions & + MasterPasswordServiceInitOptions & CryptoServiceInitOptions & ApiServiceInitOptions & TokenServiceInitOptions & @@ -111,6 +118,8 @@ export function loginStrategyServiceFactory( opts, async () => new LoginStrategyService( + await accountServiceFactory(cache, opts), + await internalMasterPasswordServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), await apiServiceFactory(cache, opts), await tokenServiceFactory(cache, opts), diff --git a/apps/browser/src/auth/background/service-factories/master-password-service.factory.ts b/apps/browser/src/auth/background/service-factories/master-password-service.factory.ts new file mode 100644 index 0000000000..a2f9052a3f --- /dev/null +++ b/apps/browser/src/auth/background/service-factories/master-password-service.factory.ts @@ -0,0 +1,42 @@ +import { + InternalMasterPasswordServiceAbstraction, + MasterPasswordServiceAbstraction, +} from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; +import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; + +import { + CachedServices, + factory, + FactoryOptions, +} from "../../../platform/background/service-factories/factory-options"; +import { + stateProviderFactory, + StateProviderInitOptions, +} from "../../../platform/background/service-factories/state-provider.factory"; + +type MasterPasswordServiceFactoryOptions = FactoryOptions; + +export type MasterPasswordServiceInitOptions = MasterPasswordServiceFactoryOptions & + StateProviderInitOptions; + +export function internalMasterPasswordServiceFactory( + cache: { masterPasswordService?: InternalMasterPasswordServiceAbstraction } & CachedServices, + opts: MasterPasswordServiceInitOptions, +): Promise { + return factory( + cache, + "masterPasswordService", + opts, + async () => new MasterPasswordService(await stateProviderFactory(cache, opts)), + ); +} + +export async function masterPasswordServiceFactory( + cache: { masterPasswordService?: InternalMasterPasswordServiceAbstraction } & CachedServices, + opts: MasterPasswordServiceInitOptions, +): Promise { + return (await internalMasterPasswordServiceFactory( + cache, + opts, + )) as MasterPasswordServiceAbstraction; +} diff --git a/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts b/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts index e8be9099ca..a8b67b21ca 100644 --- a/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts @@ -31,6 +31,11 @@ import { stateServiceFactory, } from "../../../platform/background/service-factories/state-service.factory"; +import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory"; +import { + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "./master-password-service.factory"; import { PinCryptoServiceInitOptions, pinCryptoServiceFactory } from "./pin-crypto-service.factory"; import { userDecryptionOptionsServiceFactory, @@ -46,6 +51,8 @@ type UserVerificationServiceFactoryOptions = FactoryOptions; export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryOptions & StateServiceInitOptions & CryptoServiceInitOptions & + AccountServiceInitOptions & + MasterPasswordServiceInitOptions & I18nServiceInitOptions & UserVerificationApiServiceInitOptions & UserDecryptionOptionsServiceInitOptions & @@ -66,6 +73,8 @@ export function userVerificationServiceFactory( new UserVerificationService( await stateServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), + await accountServiceFactory(cache, opts), + await internalMasterPasswordServiceFactory(cache, opts), await i18nServiceFactory(cache, opts), await userVerificationApiServiceFactory(cache, opts), await userDecryptionOptionsServiceFactory(cache, opts), diff --git a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts index 77d9741056..9a0423fca3 100644 --- a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts +++ b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts @@ -6,6 +6,7 @@ import { Subject, firstValueFrom, map, switchMap, takeUntil } from "rxjs"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -32,6 +33,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { private location: Location, private router: Router, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, + private authService: AuthService, ) {} get accountLimit() { @@ -42,13 +44,14 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { return this.accountSwitcherService.SPECIAL_ADD_ACCOUNT_ID; } - get availableAccounts$() { - return this.accountSwitcherService.availableAccounts$; - } - - get currentAccount$() { - return this.accountService.activeAccount$; - } + readonly availableAccounts$ = this.accountSwitcherService.availableAccounts$; + readonly currentAccount$ = this.accountService.activeAccount$.pipe( + switchMap((a) => + a == null + ? null + : this.authService.activeAccountStatus$.pipe(map((s) => ({ ...a, status: s }))), + ), + ); async ngOnInit() { const availableVaultTimeoutActions = await firstValueFrom( diff --git a/apps/browser/src/auth/popup/account-switching/current-account.component.ts b/apps/browser/src/auth/popup/account-switching/current-account.component.ts index 1c7f93bf30..643c37b9aa 100644 --- a/apps/browser/src/auth/popup/account-switching/current-account.component.ts +++ b/apps/browser/src/auth/popup/account-switching/current-account.component.ts @@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { Observable, combineLatest, switchMap } from "rxjs"; import { 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"; import { UserId } from "@bitwarden/common/types/guid"; @@ -29,12 +30,14 @@ export class CurrentAccountComponent { private router: Router, private location: Location, private route: ActivatedRoute, + private authService: AuthService, ) { this.currentAccount$ = combineLatest([ this.accountService.activeAccount$, this.avatarService.avatarColor$, + this.authService.activeAccountStatus$, ]).pipe( - switchMap(async ([account, avatarColor]) => { + switchMap(async ([account, avatarColor, accountStatus]) => { if (account == null) { return null; } @@ -42,7 +45,7 @@ export class CurrentAccountComponent { id: account.id, name: account.name || account.email, email: account.email, - status: account.status, + status: accountStatus, avatarColor, }; 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 f02a8ee201..fe04bee20e 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,8 @@ import { matches, mock } from "jest-mock-extended"; -import { BehaviorSubject, firstValueFrom, of, timeout } from "rxjs"; +import { BehaviorSubject, ReplaySubject, firstValueFrom, of, timeout } from "rxjs"; import { 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"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -12,22 +13,29 @@ import { UserId } from "@bitwarden/common/types/guid"; import { AccountSwitcherService } from "./account-switcher.service"; describe("AccountSwitcherService", () => { - const accountsSubject = new BehaviorSubject>(null); - const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>(null); + let accountsSubject: BehaviorSubject>; + let activeAccountSubject: BehaviorSubject<{ id: UserId } & AccountInfo>; + let authStatusSubject: ReplaySubject>; const accountService = mock(); const avatarService = mock(); const messagingService = mock(); const environmentService = mock(); const logService = mock(); + const authService = mock(); let accountSwitcherService: AccountSwitcherService; beforeEach(() => { jest.resetAllMocks(); + accountsSubject = new BehaviorSubject>(null); + activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>(null); + authStatusSubject = new ReplaySubject>(1); + // Use subject to allow for easy updates accountService.accounts$ = accountsSubject; accountService.activeAccount$ = activeAccountSubject; + authService.authStatuses$ = authStatusSubject; accountSwitcherService = new AccountSwitcherService( accountService, @@ -35,48 +43,59 @@ describe("AccountSwitcherService", () => { messagingService, environmentService, logService, + authService, ); }); + afterEach(() => { + accountsSubject.complete(); + activeAccountSubject.complete(); + authStatusSubject.complete(); + }); + describe("availableAccounts$", () => { - it("should return all accounts and an add account option when accounts are less than 5", async () => { - const user1AccountInfo: AccountInfo = { + it("should return all logged in accounts and an add account option when accounts are less than 5", async () => { + const accountInfo: AccountInfo = { name: "Test User 1", email: "test1@email.com", - status: AuthenticationStatus.Unlocked, }; avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc")); - accountsSubject.next({ - "1": user1AccountInfo, - } as Record); - - activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "1" as UserId })); + accountsSubject.next({ ["1" as UserId]: accountInfo, ["2" as UserId]: accountInfo }); + authStatusSubject.next({ + ["1" as UserId]: AuthenticationStatus.Unlocked, + ["2" as UserId]: AuthenticationStatus.Locked, + }); + activeAccountSubject.next(Object.assign(accountInfo, { id: "1" as UserId })); const accounts = await firstValueFrom( accountSwitcherService.availableAccounts$.pipe(timeout(20)), ); - expect(accounts).toHaveLength(2); + expect(accounts).toHaveLength(3); expect(accounts[0].id).toBe("1"); expect(accounts[0].isActive).toBeTruthy(); - - expect(accounts[1].id).toBe("addAccount"); + expect(accounts[1].id).toBe("2"); expect(accounts[1].isActive).toBeFalsy(); + + expect(accounts[2].id).toBe("addAccount"); + expect(accounts[2].isActive).toBeFalsy(); }); it.each([5, 6])( "should return only accounts if there are %i accounts", async (numberOfAccounts) => { const seedAccounts: Record = {}; + const seedStatuses: Record = {}; for (let i = 0; i < numberOfAccounts; i++) { seedAccounts[`${i}` as UserId] = { email: `test${i}@email.com`, name: "Test User ${i}", - status: AuthenticationStatus.Unlocked, }; + seedStatuses[`${i}` as UserId] = AuthenticationStatus.Unlocked; } avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc")); accountsSubject.next(seedAccounts); + authStatusSubject.next(seedStatuses); activeAccountSubject.next( Object.assign(seedAccounts["1" as UserId], { id: "1" as UserId }), ); @@ -89,6 +108,26 @@ describe("AccountSwitcherService", () => { }); }, ); + + it("excludes logged out accounts", async () => { + const user1AccountInfo: AccountInfo = { + name: "Test User 1", + email: "", + }; + accountsSubject.next({ ["1" as UserId]: user1AccountInfo }); + authStatusSubject.next({ ["1" as UserId]: AuthenticationStatus.LoggedOut }); + accountsSubject.next({ + "1": user1AccountInfo, + } as Record); + + const accounts = await firstValueFrom( + accountSwitcherService.availableAccounts$.pipe(timeout(20)), + ); + + // Add account only + expect(accounts).toHaveLength(1); + expect(accounts[0].id).toBe("addAccount"); + }); }); describe("selectAccount", () => { diff --git a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts index 32ebee7c75..a73ec3e1f6 100644 --- a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts +++ b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts @@ -11,6 +11,7 @@ import { } from "rxjs"; import { 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"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -48,25 +49,27 @@ export class AccountSwitcherService { private messagingService: MessagingService, private environmentService: EnvironmentService, private logService: LogService, + authService: AuthService, ) { this.availableAccounts$ = combineLatest([ - this.accountService.accounts$, + accountService.accounts$, + authService.authStatuses$, this.accountService.activeAccount$, ]).pipe( - switchMap(async ([accounts, activeAccount]) => { - const accountEntries = Object.entries(accounts).filter( - ([_, account]) => account.status !== AuthenticationStatus.LoggedOut, + switchMap(async ([accounts, accountStatuses, activeAccount]) => { + const loggedInIds = Object.keys(accounts).filter( + (id: UserId) => accountStatuses[id] !== AuthenticationStatus.LoggedOut, ); // Accounts shouldn't ever be more than ACCOUNT_LIMIT but just in case do a greater than - const hasMaxAccounts = accountEntries.length >= this.ACCOUNT_LIMIT; + const hasMaxAccounts = loggedInIds.length >= this.ACCOUNT_LIMIT; const options: AvailableAccount[] = await Promise.all( - accountEntries.map(async ([id, account]) => { + loggedInIds.map(async (id: UserId) => { return { - name: account.name ?? account.email, - email: account.email, + name: accounts[id].name ?? accounts[id].email, + email: accounts[id].email, id: id, server: (await this.environmentService.getEnvironment(id))?.getHostname(), - status: account.status, + status: accountStatuses[id], isActive: id === activeAccount?.id, avatarColor: await firstValueFrom( this.avatarService.getUserAvatarColor$(id as UserId), diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index f232eca45a..16c32337cf 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -12,6 +12,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -41,6 +42,7 @@ export class LockComponent extends BaseLockComponent { fido2PopoutSessionData$ = fido2PopoutSessionData$(); constructor( + masterPasswordService: InternalMasterPasswordServiceAbstraction, router: Router, i18nService: I18nService, platformUtilsService: PlatformUtilsService, @@ -66,6 +68,7 @@ export class LockComponent extends BaseLockComponent { accountService: AccountService, ) { super( + masterPasswordService, router, i18nService, platformUtilsService, diff --git a/apps/browser/src/auth/popup/set-password.component.ts b/apps/browser/src/auth/popup/set-password.component.ts index ea1cacc7ac..accde2e9a0 100644 --- a/apps/browser/src/auth/popup/set-password.component.ts +++ b/apps/browser/src/auth/popup/set-password.component.ts @@ -1,65 +1,9 @@ import { Component } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component"; -import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; -import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; @Component({ selector: "app-set-password", templateUrl: "set-password.component.html", }) -export class SetPasswordComponent extends BaseSetPasswordComponent { - constructor( - apiService: ApiService, - i18nService: I18nService, - cryptoService: CryptoService, - messagingService: MessagingService, - stateService: StateService, - passwordGenerationService: PasswordGenerationServiceAbstraction, - platformUtilsService: PlatformUtilsService, - policyApiService: PolicyApiServiceAbstraction, - policyService: PolicyService, - router: Router, - syncService: SyncService, - route: ActivatedRoute, - organizationApiService: OrganizationApiServiceAbstraction, - organizationUserService: OrganizationUserService, - userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, - ssoLoginService: SsoLoginServiceAbstraction, - dialogService: DialogService, - ) { - super( - i18nService, - cryptoService, - messagingService, - passwordGenerationService, - platformUtilsService, - policyApiService, - policyService, - router, - apiService, - syncService, - route, - stateService, - organizationApiService, - organizationUserService, - userDecryptionOptionsService, - ssoLoginService, - dialogService, - ); - } -} +export class SetPasswordComponent extends BaseSetPasswordComponent {} diff --git a/apps/browser/src/auth/popup/sso.component.ts b/apps/browser/src/auth/popup/sso.component.ts index 228c7401fd..14df0d1752 100644 --- a/apps/browser/src/auth/popup/sso.component.ts +++ b/apps/browser/src/auth/popup/sso.component.ts @@ -9,7 +9,9 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -45,7 +47,9 @@ export class SsoComponent extends BaseSsoComponent { logService: LogService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, configService: ConfigService, - protected authService: AuthService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, + private authService: AuthService, @Inject(WINDOW) private win: Window, ) { super( @@ -63,6 +67,8 @@ export class SsoComponent extends BaseSsoComponent { logService, userDecryptionOptionsService, configService, + masterPasswordService, + accountService, ); environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => { diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index dd541f63f8..98363bc93c 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -11,11 +11,12 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -32,8 +33,6 @@ import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { closeTwoFactorAuthPopout } from "./utils/auth-popout-window"; -const BroadcasterSubscriptionId = "TwoFactorComponent"; - @Component({ selector: "app-two-factor", templateUrl: "two-factor.component.html", @@ -50,7 +49,6 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { platformUtilsService: PlatformUtilsService, private syncService: SyncService, environmentService: EnvironmentService, - private broadcasterService: BroadcasterService, stateService: StateService, route: ActivatedRoute, private messagingService: MessagingService, @@ -62,6 +60,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { configService: ConfigService, ssoLoginService: SsoLoginServiceAbstraction, private dialogService: DialogService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, @Inject(WINDOW) protected win: Window, private browserMessagingApi: ZonedMessageListenerService, ) { @@ -82,6 +82,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { userDecryptionOptionsService, ssoLoginService, configService, + masterPasswordService, + accountService, ); super.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -175,8 +177,6 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { this.destroy$.next(); this.destroy$.complete(); - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && (await this.isLinux())) { document.body.classList.remove("linux-webauthn"); } diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index ac40bb315b..e01e2c5c02 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -1,4 +1,5 @@ import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; +import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum"; @@ -113,6 +114,7 @@ type NotificationBackgroundExtensionMessageHandlers = { bgGetEnableChangedPasswordPrompt: () => Promise; bgGetEnableAddedLoginPrompt: () => Promise; bgGetExcludedDomains: () => Promise; + bgGetActiveUserServerConfig: () => Promise; getWebVaultUrlForNotification: () => Promise; }; diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index 3b05cf57a9..45f095aee9 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -6,6 +6,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; @@ -16,7 +17,7 @@ import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; import { BrowserApi } from "../../platform/browser/browser-api"; -import { BrowserStateService } from "../../platform/services/browser-state.service"; +import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service"; import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum"; import { FormData } from "../services/abstractions/autofill.service"; import AutofillService from "../services/autofill.service"; @@ -48,12 +49,13 @@ describe("NotificationBackground", () => { const authService = mock(); const policyService = mock(); const folderService = mock(); - const stateService = mock(); + const stateService = mock(); const userNotificationSettingsService = mock(); const domainSettingsService = mock(); const environmentService = mock(); const logService = mock(); const themeStateService = mock(); + const configService = mock(); beforeEach(() => { notificationBackground = new NotificationBackground( @@ -68,6 +70,7 @@ describe("NotificationBackground", () => { environmentService, logService, themeStateService, + configService, ); }); @@ -717,7 +720,7 @@ describe("NotificationBackground", () => { ); tabSendMessageSpy = jest.spyOn(BrowserApi, "tabSendMessage").mockImplementation(); editItemSpy = jest.spyOn(notificationBackground as any, "editItem"); - setAddEditCipherInfoSpy = jest.spyOn(stateService, "setAddEditCipherInfo"); + setAddEditCipherInfoSpy = jest.spyOn(cipherService, "setAddEditCipherInfo"); openAddEditVaultItemPopoutSpy = jest.spyOn( notificationBackground as any, "openAddEditVaultItemPopout", diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index c14531ee74..9b65e4db0b 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -8,6 +8,8 @@ import { NOTIFICATION_BAR_LIFESPAN_MS } from "@bitwarden/common/autofill/constan import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -64,6 +66,7 @@ export default class NotificationBackground { bgGetEnableChangedPasswordPrompt: () => this.getEnableChangedPasswordPrompt(), bgGetEnableAddedLoginPrompt: () => this.getEnableAddedLoginPrompt(), bgGetExcludedDomains: () => this.getExcludedDomains(), + bgGetActiveUserServerConfig: () => this.getActiveUserServerConfig(), getWebVaultUrlForNotification: () => this.getWebVaultUrl(), }; @@ -79,6 +82,7 @@ export default class NotificationBackground { private environmentService: EnvironmentService, private logService: LogService, private themeStateService: ThemeStateService, + private configService: ConfigService, ) {} async init() { @@ -112,6 +116,13 @@ export default class NotificationBackground { return await firstValueFrom(this.domainSettingsService.neverDomains$); } + /** + * Gets the active user server config from the config service. + */ + async getActiveUserServerConfig(): Promise { + return await firstValueFrom(this.configService.serverConfig$); + } + /** * Checks the notification queue for any messages that need to be sent to the * specified tab. If no tab is specified, the current tab will be used. @@ -589,14 +600,14 @@ export default class NotificationBackground { } /** - * Sets the add/edit cipher info in the state service + * Sets the add/edit cipher info in the cipher service * and opens the add/edit vault item popout. * * @param cipherView - The cipher to edit * @param senderTab - The tab that the message was sent from */ private async editItem(cipherView: CipherView, senderTab: chrome.tabs.Tab) { - await this.stateService.setAddEditCipherInfo({ + await this.cipherService.setAddEditCipherInfo({ cipher: cipherView, collectionIds: cipherView.collectionIds, }); diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index c06df6603b..e65397a62b 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -33,7 +33,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { BrowserApi } from "../../platform/browser/browser-api"; -import { BrowserStateService } from "../../platform/services/browser-state.service"; +import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service"; import { BrowserPlatformUtilsService } from "../../platform/services/platform-utils/browser-platform-utils.service"; import { AutofillService } from "../services/abstractions/autofill.service"; import { @@ -72,7 +72,7 @@ describe("OverlayBackground", () => { urls: { icons: "https://icons.bitwarden.com/" }, }), ); - const stateService = mock(); + const stateService = mock(); const autofillSettingsService = mock(); const i18nService = mock(); const platformUtilsService = mock(); @@ -592,7 +592,7 @@ describe("OverlayBackground", () => { beforeEach(() => { sender = mock({ tab: { id: 1 } }); jest - .spyOn(overlayBackground["stateService"], "setAddEditCipherInfo") + .spyOn(overlayBackground["cipherService"], "setAddEditCipherInfo") .mockImplementation(); jest.spyOn(overlayBackground as any, "openAddEditVaultItemPopout").mockImplementation(); }); @@ -600,7 +600,7 @@ describe("OverlayBackground", () => { it("will not open the add edit popout window if the message does not have a login cipher provided", () => { sendExtensionRuntimeMessage({ command: "autofillOverlayAddNewVaultItem" }, sender); - expect(overlayBackground["stateService"].setAddEditCipherInfo).not.toHaveBeenCalled(); + expect(overlayBackground["cipherService"].setAddEditCipherInfo).not.toHaveBeenCalled(); expect(overlayBackground["openAddEditVaultItemPopout"]).not.toHaveBeenCalled(); }); @@ -621,7 +621,7 @@ describe("OverlayBackground", () => { ); await flushPromises(); - expect(overlayBackground["stateService"].setAddEditCipherInfo).toHaveBeenCalled(); + expect(overlayBackground["cipherService"].setAddEditCipherInfo).toHaveBeenCalled(); expect(BrowserApi.sendMessage).toHaveBeenCalledWith( "inlineAutofillMenuRefreshAddEditCipher", ); diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 50fb80ef1b..551263525e 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -636,7 +636,7 @@ class OverlayBackground implements OverlayBackgroundInterface { cipherView.type = CipherType.Login; cipherView.login = loginView; - await this.stateService.setAddEditCipherInfo({ + await this.cipherService.setAddEditCipherInfo({ cipher: cipherView, collectionIds: cipherView.collectionIds, }); diff --git a/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts b/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts index c948f7aa94..bee5da18b5 100644 --- a/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts +++ b/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts @@ -1,3 +1,7 @@ +import { + accountServiceFactory, + AccountServiceInitOptions, +} from "../../../auth/background/service-factories/account-service.factory"; import { UserVerificationServiceInitOptions, userVerificationServiceFactory, @@ -7,6 +11,10 @@ import { eventCollectionServiceFactory, } from "../../../background/service-factories/event-collection-service.factory"; import { billingAccountProfileStateServiceFactory } from "../../../platform/background/service-factories/billing-account-profile-state-service.factory"; +import { + browserScriptInjectorServiceFactory, + BrowserScriptInjectorServiceInitOptions, +} from "../../../platform/background/service-factories/browser-script-injector-service.factory"; import { CachedServices, factory, @@ -45,7 +53,9 @@ export type AutoFillServiceInitOptions = AutoFillServiceOptions & EventCollectionServiceInitOptions & LogServiceInitOptions & UserVerificationServiceInitOptions & - DomainSettingsServiceInitOptions; + DomainSettingsServiceInitOptions & + BrowserScriptInjectorServiceInitOptions & + AccountServiceInitOptions; export function autofillServiceFactory( cache: { autofillService?: AbstractAutoFillService } & CachedServices, @@ -65,6 +75,8 @@ export function autofillServiceFactory( await domainSettingsServiceFactory(cache, opts), await userVerificationServiceFactory(cache, opts), await billingAccountProfileStateServiceFactory(cache, opts), + await browserScriptInjectorServiceFactory(cache, opts), + await accountServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/autofill/content/autofill-init.spec.ts b/apps/browser/src/autofill/content/autofill-init.spec.ts index b299ddccbf..8f1a8bf992 100644 --- a/apps/browser/src/autofill/content/autofill-init.spec.ts +++ b/apps/browser/src/autofill/content/autofill-init.spec.ts @@ -560,6 +560,17 @@ describe("AutofillInit", () => { }); describe("destroy", () => { + it("clears the timeout used to collect page details on load", () => { + jest.spyOn(window, "clearTimeout"); + + autofillInit.init(); + autofillInit.destroy(); + + expect(window.clearTimeout).toHaveBeenCalledWith( + autofillInit["collectPageDetailsOnLoadTimeout"], + ); + }); + it("removes the extension message listeners", () => { autofillInit.destroy(); diff --git a/apps/browser/src/autofill/content/autofill-init.ts b/apps/browser/src/autofill/content/autofill-init.ts index 2de35dee20..e78a1fb5ee 100644 --- a/apps/browser/src/autofill/content/autofill-init.ts +++ b/apps/browser/src/autofill/content/autofill-init.ts @@ -16,6 +16,7 @@ class AutofillInit implements AutofillInitInterface { private readonly domElementVisibilityService: DomElementVisibilityService; private readonly collectAutofillContentService: CollectAutofillContentService; private readonly insertAutofillContentService: InsertAutofillContentService; + private collectPageDetailsOnLoadTimeout: number | NodeJS.Timeout | undefined; private readonly extensionMessageHandlers: AutofillExtensionMessageHandlers = { collectPageDetails: ({ message }) => this.collectPageDetails(message), collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true), @@ -66,17 +67,19 @@ class AutofillInit implements AutofillInitInterface { * to act on the page. */ private collectPageDetailsOnLoad() { - const sendCollectDetailsMessage = () => - setTimeout( + const sendCollectDetailsMessage = () => { + this.clearCollectPageDetailsOnLoadTimeout(); + this.collectPageDetailsOnLoadTimeout = setTimeout( () => sendExtensionMessage("bgCollectPageDetails", { sender: "autofillInit" }), 250, ); + }; - if (document.readyState === "complete") { + if (globalThis.document.readyState === "complete") { sendCollectDetailsMessage(); } - window.addEventListener("load", sendCollectDetailsMessage); + globalThis.addEventListener("load", sendCollectDetailsMessage); } /** @@ -247,6 +250,15 @@ class AutofillInit implements AutofillInitInterface { this.autofillOverlayContentService.autofillOverlayVisibility = data?.autofillOverlayVisibility; } + /** + * Clears the send collect details message timeout. + */ + private clearCollectPageDetailsOnLoadTimeout() { + if (this.collectPageDetailsOnLoadTimeout) { + clearTimeout(this.collectPageDetailsOnLoadTimeout); + } + } + /** * Sets up the extension message listeners for the content script. */ @@ -288,6 +300,7 @@ class AutofillInit implements AutofillInitInterface { * listeners, timeouts, and object instances to prevent memory leaks. */ destroy() { + this.clearCollectPageDetailsOnLoadTimeout(); chrome.runtime.onMessage.removeListener(this.handleExtensionMessage); this.collectAutofillContentService.destroy(); this.autofillOverlayContentService?.destroy(); diff --git a/apps/browser/src/autofill/content/notification-bar.ts b/apps/browser/src/autofill/content/notification-bar.ts index 8c1ef93c32..2bcf4394fd 100644 --- a/apps/browser/src/autofill/content/notification-bar.ts +++ b/apps/browser/src/autofill/content/notification-bar.ts @@ -1,3 +1,4 @@ +import { ServerConfig } from "../../../../../libs/common/src/platform/abstractions/config/server-config"; import { AddLoginMessageData, ChangePasswordMessageData, @@ -6,12 +7,7 @@ import AutofillField from "../models/autofill-field"; import { WatchedForm } from "../models/watched-form"; import { NotificationBarIframeInitData } from "../notification/abstractions/notification-bar"; import { FormData } from "../services/abstractions/autofill.service"; -import { UserSettings } from "../types"; -import { - getFromLocalStorage, - sendExtensionMessage, - setupExtensionDisconnectAction, -} from "../utils"; +import { sendExtensionMessage, setupExtensionDisconnectAction } from "../utils"; interface HTMLElementWithFormOpId extends HTMLElement { formOpId: string; @@ -95,25 +91,17 @@ async function loadNotificationBar() { ); const enableAddedLoginPrompt = await sendExtensionMessage("bgGetEnableAddedLoginPrompt"); const excludedDomains = await sendExtensionMessage("bgGetExcludedDomains"); + const activeUserServerConfig: ServerConfig = await sendExtensionMessage( + "bgGetActiveUserServerConfig", + ); + const activeUserVault = activeUserServerConfig?.environment?.vault; let showNotificationBar = true; - // Look up the active user id from storage - const activeUserIdKey = "activeUserId"; - let activeUserId: string; - - const activeUserStorageValue = await getFromLocalStorage(activeUserIdKey); - if (activeUserStorageValue[activeUserIdKey]) { - activeUserId = activeUserStorageValue[activeUserIdKey]; - } - - // Look up the user's settings from storage - const userSettingsStorageValue = await getFromLocalStorage(activeUserId); - if (userSettingsStorageValue[activeUserId]) { - const userSettings: UserSettings = userSettingsStorageValue[activeUserId].settings; + if (activeUserVault) { // Do not show the notification bar on the Bitwarden vault // because they can add logins and change passwords there - if (window.location.origin === userSettings.serverConfig.environment.vault) { + if (window.location.origin === activeUserVault) { showNotificationBar = false; } else { // NeverDomains is a dictionary of domains that the user has chosen to never diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index 4db64f417d..d1fbf79bfa 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -32,6 +32,7 @@ import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { BrowserApi } from "../../platform/browser/browser-api"; +import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import { AutofillPort } from "../enums/autofill-port.enums"; import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; @@ -67,6 +68,7 @@ describe("AutofillService", () => { const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService); let domainSettingsService: DomainSettingsService; + let scriptInjectorService: BrowserScriptInjectorService; const totpService = mock(); const eventCollectionService = mock(); const logService = mock(); @@ -74,6 +76,7 @@ describe("AutofillService", () => { const billingAccountProfileStateService = mock(); beforeEach(() => { + scriptInjectorService = new BrowserScriptInjectorService(); autofillService = new AutofillService( cipherService, autofillSettingsService, @@ -83,6 +86,8 @@ describe("AutofillService", () => { domainSettingsService, userVerificationService, billingAccountProfileStateService, + scriptInjectorService, + accountService, ); domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); @@ -250,6 +255,7 @@ describe("AutofillService", () => { expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, { file: "content/content-message-handler.js", + frameId: 0, ...defaultExecuteScriptOptions, }); }); diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 8b33d03419..8f85d65692 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -1,7 +1,9 @@ import { firstValueFrom } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; @@ -20,6 +22,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; import { BrowserApi } from "../../platform/browser/browser-api"; +import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; import { openVaultItemPasswordRepromptPopout } from "../../vault/popup/utils/vault-popout-window"; import { AutofillPort } from "../enums/autofill-port.enums"; import AutofillField from "../models/autofill-field"; @@ -55,6 +58,8 @@ export default class AutofillService implements AutofillServiceInterface { private domainSettingsService: DomainSettingsService, private userVerificationService: UserVerificationService, private billingAccountProfileStateService: BillingAccountProfileStateService, + private scriptInjectorService: ScriptInjectorService, + private accountService: AccountService, ) {} /** @@ -102,30 +107,46 @@ export default class AutofillService implements AutofillServiceInterface { frameId = 0, triggeringOnPageLoad = true, ): Promise { - const mainAutofillScript = (await this.getOverlayVisibility()) + // Autofill settings loaded from state can await the active account state indefinitely if + // not guarded by an active account check (e.g. the user is logged in) + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + + // These settings are not available until the user logs in + let overlayVisibility: InlineMenuVisibilitySetting = AutofillOverlayVisibility.Off; + let autoFillOnPageLoadIsEnabled = false; + + if (activeAccount) { + overlayVisibility = await this.getOverlayVisibility(); + } + const mainAutofillScript = overlayVisibility ? "bootstrap-autofill-overlay.js" : "bootstrap-autofill.js"; const injectedScripts = [mainAutofillScript]; - const autoFillOnPageLoadIsEnabled = await this.getAutofillOnPageLoad(); + if (activeAccount) { + autoFillOnPageLoadIsEnabled = await this.getAutofillOnPageLoad(); + } if (triggeringOnPageLoad && autoFillOnPageLoadIsEnabled) { injectedScripts.push("autofiller.js"); } else { - await BrowserApi.executeScriptInTab(tab.id, { - file: "content/content-message-handler.js", - runAt: "document_start", + await this.scriptInjectorService.inject({ + tabId: tab.id, + injectDetails: { file: "content/content-message-handler.js", runAt: "document_start" }, }); } injectedScripts.push("notificationBar.js", "contextMenuHandler.js"); for (const injectedScript of injectedScripts) { - await BrowserApi.executeScriptInTab(tab.id, { - file: `content/${injectedScript}`, - frameId, - runAt: "document_start", + await this.scriptInjectorService.inject({ + tabId: tab.id, + injectDetails: { + file: `content/${injectedScript}`, + runAt: "document_start", + frame: frameId, + }, }); } } diff --git a/apps/browser/src/autofill/spec/autofill-mocks.ts b/apps/browser/src/autofill/spec/autofill-mocks.ts index 708489c57e..9c957f6b1b 100644 --- a/apps/browser/src/autofill/spec/autofill-mocks.ts +++ b/apps/browser/src/autofill/spec/autofill-mocks.ts @@ -267,6 +267,7 @@ function createPortSpyMock(name: string) { disconnect: jest.fn(), sender: { tab: createChromeTabMock(), + url: "https://jest-testing-website.com", }, }); } diff --git a/apps/browser/src/autofill/spec/fido2-testing-utils.ts b/apps/browser/src/autofill/spec/fido2-testing-utils.ts new file mode 100644 index 0000000000..c9b39c16cc --- /dev/null +++ b/apps/browser/src/autofill/spec/fido2-testing-utils.ts @@ -0,0 +1,74 @@ +import { mock } from "jest-mock-extended"; + +import { + AssertCredentialResult, + CreateCredentialResult, +} from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction"; + +export function createCredentialCreationOptionsMock( + customFields: Partial = {}, +): CredentialCreationOptions { + return mock({ + publicKey: mock({ + authenticatorSelection: { authenticatorAttachment: "platform" }, + excludeCredentials: [{ id: new ArrayBuffer(32), type: "public-key" }], + pubKeyCredParams: [{ alg: -7, type: "public-key" }], + user: { id: new ArrayBuffer(32), name: "test", displayName: "test" }, + }), + ...customFields, + }); +} + +export function createCreateCredentialResultMock( + customFields: Partial = {}, +): CreateCredentialResult { + return mock({ + credentialId: "mock", + clientDataJSON: "mock", + attestationObject: "mock", + authData: "mock", + publicKey: "mock", + publicKeyAlgorithm: -7, + transports: ["internal"], + ...customFields, + }); +} + +export function createCredentialRequestOptionsMock( + customFields: Partial = {}, +): CredentialRequestOptions { + return mock({ + mediation: "optional", + publicKey: mock({ + allowCredentials: [{ id: new ArrayBuffer(32), type: "public-key" }], + }), + ...customFields, + }); +} + +export function createAssertCredentialResultMock( + customFields: Partial = {}, +): AssertCredentialResult { + return mock({ + credentialId: "mock", + clientDataJSON: "mock", + authenticatorData: "mock", + signature: "mock", + userHandle: "mock", + ...customFields, + }); +} + +export function setupMockedWebAuthnSupport() { + (globalThis as any).PublicKeyCredential = class PolyfillPublicKeyCredential { + static isUserVerifyingPlatformAuthenticatorAvailable = () => Promise.resolve(true); + }; + (globalThis as any).AuthenticatorAttestationResponse = + class PolyfillAuthenticatorAttestationResponse {}; + (globalThis as any).AuthenticatorAssertionResponse = + class PolyfillAuthenticatorAssertionResponse {}; + (globalThis as any).navigator.credentials = { + create: jest.fn().mockResolvedValue({}), + get: jest.fn().mockResolvedValue({}), + }; +} diff --git a/apps/browser/src/autofill/utils/index.spec.ts b/apps/browser/src/autofill/utils/index.spec.ts index 2fe8496b8d..af67d41601 100644 --- a/apps/browser/src/autofill/utils/index.spec.ts +++ b/apps/browser/src/autofill/utils/index.spec.ts @@ -8,7 +8,6 @@ import { generateRandomCustomElementName, sendExtensionMessage, setElementStyles, - getFromLocalStorage, setupExtensionDisconnectAction, setupAutofillInitDisconnectAction, } from "./index"; @@ -124,33 +123,6 @@ describe("setElementStyles", () => { }); }); -describe("getFromLocalStorage", () => { - it("returns a promise with the storage object pulled from the extension storage api", async () => { - const localStorage: Record = { - testValue: "test", - another: "another", - }; - jest.spyOn(chrome.storage.local, "get").mockImplementation((keys, callback) => { - const localStorageObject: Record = {}; - - if (typeof keys === "string") { - localStorageObject[keys] = localStorage[keys]; - } else if (Array.isArray(keys)) { - for (const key of keys) { - localStorageObject[key] = localStorage[key]; - } - } - - callback(localStorageObject); - }); - - const returnValue = await getFromLocalStorage("testValue"); - - expect(chrome.storage.local.get).toHaveBeenCalled(); - expect(returnValue).toEqual({ testValue: "test" }); - }); -}); - describe("setupExtensionDisconnectAction", () => { afterEach(() => { jest.clearAllMocks(); diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts index 2644425d70..72e7f9ab62 100644 --- a/apps/browser/src/autofill/utils/index.ts +++ b/apps/browser/src/autofill/utils/index.ts @@ -106,18 +106,6 @@ function setElementStyles( } } -/** - * Get data from local storage based on the keys provided. - * - * @param keys - String or array of strings of keys to get from local storage - * @deprecated Do not call this, use state-relevant services instead - */ -async function getFromLocalStorage(keys: string | string[]): Promise> { - return new Promise((resolve) => { - chrome.storage.local.get(keys, (storage: Record) => resolve(storage)); - }); -} - /** * Sets up a long-lived connection with the extension background * and triggers an onDisconnect event if the extension context @@ -278,7 +266,6 @@ export { buildSvgDomElement, sendExtensionMessage, setElementStyles, - getFromLocalStorage, setupExtensionDisconnectAction, setupAutofillInitDisconnectAction, elementIsFillableFormField, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 30b6d74333..6c6e8b8a98 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1,4 +1,4 @@ -import { firstValueFrom } from "rxjs"; +import { Subject, firstValueFrom, merge } from "rxjs"; import { PinCryptoServiceAbstraction, @@ -10,6 +10,7 @@ import { AuthRequestServiceAbstraction, AuthRequestService, LoginEmailServiceAbstraction, + LoginEmailService, } from "@bitwarden/auth/common"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; @@ -28,10 +29,12 @@ import { PolicyService } from "@bitwarden/common/admin-console/services/policy/p import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -46,6 +49,7 @@ import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; +import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; @@ -79,7 +83,6 @@ import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/co import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { AbstractMemoryStorageService, @@ -92,6 +95,9 @@ import { DefaultBiometricStateService, } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- Used for dependency creation +import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; @@ -107,6 +113,7 @@ import { MigrationBuilderService } from "@bitwarden/common/platform/services/mig import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { SystemService } from "@bitwarden/common/platform/services/system.service"; +import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { ActiveUserStateProvider, @@ -131,7 +138,6 @@ import { EventUploadService } from "@bitwarden/common/services/event/event-uploa import { NotificationsService } from "@bitwarden/common/services/notifications.service"; import { SearchService } from "@bitwarden/common/services/search.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; -import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/avatar.service"; import { PasswordGenerationService, PasswordGenerationServiceAbstraction, @@ -204,25 +210,29 @@ import { Account } from "../models/account"; import { BrowserApi } from "../platform/browser/browser-api"; import { flagEnabled } from "../platform/flags"; import { UpdateBadge } from "../platform/listeners/update-badge"; +/* eslint-disable no-restricted-imports */ +import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender"; +/* eslint-enable no-restricted-imports */ import { BrowserStateService as StateServiceAbstraction } from "../platform/services/abstractions/browser-state.service"; import { BrowserCryptoService } from "../platform/services/browser-crypto.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 BrowserMessagingPrivateModeBackgroundService from "../platform/services/browser-messaging-private-mode-background.service"; -import BrowserMessagingService from "../platform/services/browser-messaging.service"; -import { BrowserStateService } from "../platform/services/browser-state.service"; +import { BrowserScriptInjectorService } from "../platform/services/browser-script-injector.service"; +import { DefaultBrowserStateService } from "../platform/services/default-browser-state.service"; import I18nService from "../platform/services/i18n.service"; import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service"; import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; +import { ForegroundMemoryStorageService } from "../platform/storage/foreground-memory-storage.service"; +import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; import FilelessImporterBackground from "../tools/background/fileless-importer.background"; +import { Fido2Background as Fido2BackgroundAbstraction } from "../vault/fido2/background/abstractions/fido2.background"; +import { Fido2Background } from "../vault/fido2/background/fido2.background"; import { BrowserFido2UserInterfaceService } from "../vault/fido2/browser-fido2-user-interface.service"; -import { Fido2Service as Fido2ServiceAbstraction } from "../vault/services/abstractions/fido2.service"; -import Fido2Service from "../vault/services/fido2.service"; import { VaultFilterService } from "../vault/services/vault-filter.service"; import CommandsBackground from "./commands.background"; @@ -231,8 +241,8 @@ import { NativeMessagingBackground } from "./nativeMessaging.background"; import RuntimeBackground from "./runtime.background"; export default class MainBackground { - messagingService: MessagingServiceAbstraction; - storageService: AbstractStorageService & ObservableStorageService; + messagingService: MessageSender; + storageService: BrowserLocalStorageService; secureStorageService: AbstractStorageService; memoryStorageService: AbstractMemoryStorageService; memoryStorageForStateProviders: AbstractMemoryStorageService & ObservableStorageService; @@ -242,6 +252,7 @@ export default class MainBackground { keyGenerationService: KeyGenerationServiceAbstraction; cryptoService: CryptoServiceAbstraction; cryptoFunctionService: CryptoFunctionServiceAbstraction; + masterPasswordService: InternalMasterPasswordServiceAbstraction; tokenService: TokenServiceAbstraction; appIdService: AppIdServiceAbstraction; apiService: ApiServiceAbstraction; @@ -312,7 +323,7 @@ export default class MainBackground { activeUserStateProvider: ActiveUserStateProvider; derivedStateProvider: DerivedStateProvider; stateProvider: StateProvider; - fido2Service: Fido2ServiceAbstraction; + fido2Background: Fido2BackgroundAbstraction; individualVaultExportService: IndividualVaultExportServiceAbstraction; organizationVaultExportService: OrganizationVaultExportServiceAbstraction; vaultSettingsService: VaultSettingsServiceAbstraction; @@ -320,6 +331,10 @@ export default class MainBackground { stateEventRunnerService: StateEventRunnerService; ssoLoginService: SsoLoginServiceAbstraction; billingAccountProfileStateService: BillingAccountProfileStateService; + // eslint-disable-next-line rxjs/no-exposed-subjects -- Needed to give access to services module + intraprocessMessagingSubject: Subject>; + userKeyInitService: UserKeyInitService; + scriptInjectorService: BrowserScriptInjectorService; onUpdatedRan: boolean; onReplacedRan: boolean; @@ -338,11 +353,11 @@ export default class MainBackground { private syncTimeout: any; private isSafari: boolean; private nativeMessagingBackground: NativeMessagingBackground; - popupOnlyContext: boolean; - - constructor(public isPrivateMode: boolean = false) { - this.popupOnlyContext = isPrivateMode || BrowserApi.isManifestVersion(3); + constructor( + public isPrivateMode: boolean = false, + public popupOnlyContext: boolean = false, + ) { // Services const lockedCallback = async (userId?: string) => { if (this.notificationsService != null) { @@ -361,21 +376,45 @@ export default class MainBackground { const logoutCallback = async (expired: boolean, userId?: UserId) => await this.logout(expired, userId); - this.messagingService = this.popupOnlyContext - ? new BrowserMessagingPrivateModeBackgroundService() - : new BrowserMessagingService(); this.logService = new ConsoleLogService(false); this.cryptoFunctionService = new WebCryptoFunctionService(self); this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); this.storageService = new BrowserLocalStorageService(); + this.intraprocessMessagingSubject = new Subject>(); + + this.messagingService = MessageSender.combine( + new SubjectMessageSender(this.intraprocessMessagingSubject), + new ChromeMessageSender(this.logService), + ); + + const messageListener = new MessageListener( + merge( + this.intraprocessMessagingSubject.asObservable(), // For messages from the same context + fromChromeRuntimeMessaging(), // For messages from other contexts + ), + ); + + this.platformUtilsService = new BackgroundPlatformUtilsService( + this.messagingService, + (clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs), + async () => this.biometricUnlock(), + self, + ); + const mv3MemoryStorageCreator = (partitionName: string) => { + if (this.popupOnlyContext) { + return new ForegroundMemoryStorageService(partitionName); + } + // TODO: Consider using multithreaded encrypt service in popup only context return new LocalBackedSessionStorageService( + this.logService, new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false), this.keyGenerationService, new BrowserLocalStorageService(), new BrowserMemoryStorageService(), + this.platformUtilsService, partitionName, ); }; @@ -405,13 +444,14 @@ export default class MainBackground { storageServiceProvider, ); - this.encryptService = flagEnabled("multithreadDecryption") - ? new MultithreadEncryptServiceImplementation( - this.cryptoFunctionService, - this.logService, - true, - ) - : new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true); + this.encryptService = + flagEnabled("multithreadDecryption") && BrowserApi.isManifestVersion(2) + ? new MultithreadEncryptServiceImplementation( + this.cryptoFunctionService, + this.logService, + true, + ) + : new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true); this.singleUserStateProvider = new DefaultSingleUserStateProvider( storageServiceProvider, @@ -443,12 +483,6 @@ export default class MainBackground { this.biometricStateService = new DefaultBiometricStateService(this.stateProvider); this.userNotificationSettingsService = new UserNotificationSettingsService(this.stateProvider); - this.platformUtilsService = new BackgroundPlatformUtilsService( - this.messagingService, - (clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs), - async () => this.biometricUnlock(), - self, - ); this.tokenService = new TokenService( this.singleUserStateProvider, @@ -466,7 +500,7 @@ export default class MainBackground { new MigrationBuilderService(), ); - this.stateService = new BrowserStateService( + this.stateService = new DefaultBrowserStateService( this.storageService, this.secureStorageService, this.memoryStorageService, @@ -480,8 +514,11 @@ export default class MainBackground { const themeStateService = new DefaultThemeStateService(this.globalStateProvider); + this.masterPasswordService = new MasterPasswordService(this.stateProvider); + this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); this.cryptoService = new BrowserCryptoService( + this.masterPasswordService, this.keyGenerationService, this.cryptoFunctionService, this.encryptService, @@ -508,7 +545,7 @@ export default class MainBackground { this.apiService, this.fileUploadService, ); - this.searchService = new SearchService(this.logService, this.i18nService); + this.searchService = new SearchService(this.logService, this.i18nService, this.stateProvider); this.collectionService = new CollectionService( this.cryptoService, @@ -525,6 +562,8 @@ export default class MainBackground { this.badgeSettingsService = new BadgeSettingsService(this.stateProvider); this.policyApiService = new PolicyApiService(this.policyService, this.apiService); this.keyConnectorService = new KeyConnectorService( + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -545,18 +584,6 @@ export default class MainBackground { this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService); - // eslint-disable-next-line - const that = this; - const backgroundMessagingService = new (class extends MessagingServiceAbstraction { - // AuthService should send the messages to the background not popup. - send = (subscriber: string, arg: any = {}) => { - const message = Object.assign({}, { command: subscriber }, arg); - // 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 - that.runtimeBackground.processMessage(message, that as any); - }; - })(); - this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); @@ -578,14 +605,16 @@ export default class MainBackground { this.authRequestService = new AuthRequestService( this.appIdService, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, - this.stateService, + this.stateProvider, ); this.authService = new AuthService( this.accountService, - backgroundMessagingService, + this.messagingService, this.cryptoService, this.apiService, this.stateService, @@ -596,13 +625,17 @@ export default class MainBackground { this.stateProvider, ); + this.loginEmailService = new LoginEmailService(this.stateProvider); + this.loginStrategyService = new LoginStrategyService( + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, this.appIdService, this.platformUtilsService, - backgroundMessagingService, + this.messagingService, this.logService, this.keyConnectorService, this.environmentService, @@ -643,12 +676,12 @@ export default class MainBackground { this.encryptService, this.cipherFileUploadService, this.configService, + this.stateProvider, ); this.folderService = new FolderService( this.cryptoService, this.i18nService, this.cipherService, - this.stateService, this.stateProvider, ); this.folderApiService = new FolderApiService(this.folderService, this.apiService); @@ -672,6 +705,8 @@ export default class MainBackground { this.userVerificationService = new UserVerificationService( this.stateService, this.cryptoService, + this.accountService, + this.masterPasswordService, this.i18nService, this.userVerificationApiService, this.userDecryptionOptionsService, @@ -694,6 +729,8 @@ export default class MainBackground { this.vaultSettingsService = new VaultSettingsService(this.stateProvider); this.vaultTimeoutService = new VaultTimeoutService( + this.accountService, + this.masterPasswordService, this.cipherService, this.folderService, this.collectionService, @@ -729,6 +766,8 @@ export default class MainBackground { this.providerService = new ProviderService(this.stateProvider); this.syncService = new SyncService( + this.masterPasswordService, + this.accountService, this.apiService, this.domainSettingsService, this.folderService, @@ -749,22 +788,24 @@ export default class MainBackground { this.avatarService, logoutCallback, this.billingAccountProfileStateService, + this.tokenService, ); this.eventUploadService = new EventUploadService( this.apiService, this.stateProvider, this.logService, - this.accountService, + this.authService, ); this.eventCollectionService = new EventCollectionService( this.cipherService, this.stateProvider, this.organizationService, this.eventUploadService, - this.accountService, + this.authService, ); this.totpService = new TotpService(this.cryptoFunctionService, this.logService); + this.scriptInjectorService = new BrowserScriptInjectorService(); this.autofillService = new AutofillService( this.cipherService, this.autofillSettingsService, @@ -774,6 +815,8 @@ export default class MainBackground { this.domainSettingsService, this.userVerificationService, this.billingAccountProfileStateService, + this.scriptInjectorService, + this.accountService, ); this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); @@ -819,10 +862,10 @@ export default class MainBackground { logoutCallback, this.stateService, this.authService, + this.authRequestService, this.messagingService, ); - this.fido2Service = new Fido2Service(); this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.authService); this.fido2AuthenticatorService = new Fido2AuthenticatorService( this.cipherService, @@ -862,79 +905,90 @@ export default class MainBackground { this.isSafari = this.platformUtilsService.isSafari(); // Background - this.runtimeBackground = new RuntimeBackground( - this, - this.autofillService, - this.platformUtilsService as BrowserPlatformUtilsService, - this.i18nService, - this.notificationsService, - this.stateService, - this.autofillSettingsService, - this.systemService, - this.environmentService, - this.messagingService, - this.logService, - this.configService, - this.fido2Service, - ); - this.nativeMessagingBackground = new NativeMessagingBackground( - this.cryptoService, - this.cryptoFunctionService, - this.runtimeBackground, - this.messagingService, - this.appIdService, - this.platformUtilsService, - this.stateService, - this.logService, - this.authService, - this.biometricStateService, - ); - this.commandsBackground = new CommandsBackground( - this, - this.passwordGenerationService, - this.platformUtilsService, - this.vaultTimeoutService, - this.authService, - ); - this.notificationBackground = new NotificationBackground( - this.autofillService, - this.cipherService, - this.authService, - this.policyService, - this.folderService, - this.stateService, - this.userNotificationSettingsService, - this.domainSettingsService, - this.environmentService, - this.logService, - themeStateService, - ); - this.overlayBackground = new OverlayBackground( - this.cipherService, - this.autofillService, - this.authService, - this.environmentService, - this.domainSettingsService, - this.stateService, - this.autofillSettingsService, - this.i18nService, - this.platformUtilsService, - themeStateService, - ); - this.filelessImporterBackground = new FilelessImporterBackground( - this.configService, - this.authService, - this.policyService, - this.notificationBackground, - this.importService, - this.syncService, - ); - this.tabsBackground = new TabsBackground( - this, - this.notificationBackground, - this.overlayBackground, - ); if (!this.popupOnlyContext) { + this.fido2Background = new Fido2Background( + this.logService, + this.fido2ClientService, + this.vaultSettingsService, + this.scriptInjectorService, + ); + this.runtimeBackground = new RuntimeBackground( + this, + this.autofillService, + this.platformUtilsService as BrowserPlatformUtilsService, + this.notificationsService, + this.stateService, + this.autofillSettingsService, + this.systemService, + this.environmentService, + this.messagingService, + this.logService, + this.configService, + this.fido2Background, + messageListener, + ); + this.nativeMessagingBackground = new NativeMessagingBackground( + this.accountService, + this.masterPasswordService, + this.cryptoService, + this.cryptoFunctionService, + this.runtimeBackground, + this.messagingService, + this.appIdService, + this.platformUtilsService, + this.stateService, + this.logService, + this.authService, + this.biometricStateService, + ); + this.commandsBackground = new CommandsBackground( + this, + this.passwordGenerationService, + this.platformUtilsService, + this.vaultTimeoutService, + this.authService, + ); + this.notificationBackground = new NotificationBackground( + this.autofillService, + this.cipherService, + this.authService, + this.policyService, + this.folderService, + this.stateService, + this.userNotificationSettingsService, + this.domainSettingsService, + this.environmentService, + this.logService, + themeStateService, + this.configService, + ); + this.overlayBackground = new OverlayBackground( + this.cipherService, + this.autofillService, + this.authService, + this.environmentService, + this.domainSettingsService, + this.stateService, + this.autofillSettingsService, + this.i18nService, + this.platformUtilsService, + themeStateService, + ); + this.filelessImporterBackground = new FilelessImporterBackground( + this.configService, + this.authService, + this.policyService, + this.notificationBackground, + this.importService, + this.syncService, + this.scriptInjectorService, + ); + this.tabsBackground = new TabsBackground( + this, + this.notificationBackground, + this.overlayBackground, + ); + const contextMenuClickedHandler = new ContextMenuClickedHandler( (options) => this.platformUtilsService.copyToClipboard(options.text), async (_tab) => { @@ -976,11 +1030,6 @@ export default class MainBackground { this.notificationsService, this.accountService, ); - this.webRequestBackground = new WebRequestBackground( - this.platformUtilsService, - this.cipherService, - this.authService, - ); this.usernameGenerationService = new UsernameGenerationService( this.cryptoService, @@ -1002,7 +1051,21 @@ export default class MainBackground { this.authService, this.cipherService, ); + + if (BrowserApi.isManifestVersion(2)) { + this.webRequestBackground = new WebRequestBackground( + this.platformUtilsService, + this.cipherService, + this.authService, + ); + } } + + this.userKeyInitService = new UserKeyInitService( + this.accountService, + this.cryptoService, + this.logService, + ); } async bootstrap() { @@ -1011,26 +1074,29 @@ export default class MainBackground { // Only the "true" background should run migrations await this.stateService.init({ runMigrations: !this.isPrivateMode }); - await this.vaultTimeoutService.init(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. + this.userKeyInitService.listenForActiveUserChangesToSetUserKey(); + await (this.i18nService as I18nService).init(); await (this.eventUploadService as EventUploadService).init(true); - await this.runtimeBackground.init(); - await this.notificationBackground.init(); - this.filelessImporterBackground.init(); - await this.commandsBackground.init(); - this.twoFactorService.init(); - await this.overlayBackground.init(); - - await this.tabsBackground.init(); if (!this.popupOnlyContext) { + await this.vaultTimeoutService.init(true); + this.fido2Background.init(); + await this.runtimeBackground.init(); + await this.notificationBackground.init(); + this.filelessImporterBackground.init(); + await this.commandsBackground.init(); + await this.overlayBackground.init(); + await this.tabsBackground.init(); this.contextMenusBackground?.init(); + await this.idleBackground.init(); + if (BrowserApi.isManifestVersion(2)) { + await this.webRequestBackground.init(); + } } - await this.idleBackground.init(); - await this.webRequestBackground.init(); - - await this.fido2Service.init(); if (this.platformUtilsService.isFirefox() && !this.isPrivateMode) { // Set Private Mode windows to the default icon - they do not share state with the background page @@ -1053,9 +1119,7 @@ export default class MainBackground { if (!this.isPrivateMode) { await this.refreshBadge(); } - // 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.fullSync(true); + await this.fullSync(true); setTimeout(() => this.notificationsService.init(), 2500); resolve(); }, 500); @@ -1107,7 +1171,7 @@ export default class MainBackground { const status = await this.authService.getAuthStatus(userId); const forcePasswordReset = - (await this.stateService.getForceSetPasswordReason({ userId: userId })) != + (await firstValueFrom(this.masterPasswordService.forceSetPasswordReason$(userId))) != ForceSetPasswordReason.None; await this.systemService.clearPendingClipboard(); @@ -1140,12 +1204,10 @@ export default class MainBackground { this.cipherService.clear(userId), this.folderService.clear(userId), this.collectionService.clear(userId), - this.policyService.clear(userId), this.passwordGenerationService.clear(userId), this.vaultTimeoutSettingsService.clear(userId), this.vaultFilterService.clear(), this.biometricStateService.logout(userId), - this.providerService.save(null, userId), /* We intentionally do not clear: * - autofillSettingsService * - badgeSettingsService @@ -1156,14 +1218,9 @@ export default class MainBackground { //Needs to be checked before state is cleaned const needStorageReseed = await this.needsStorageReseed(); - const currentUserId = await this.stateService.getUserId(); const newActiveUser = await this.stateService.clean({ userId: userId }); - if (userId == null || userId === currentUserId) { - this.searchService.clearIndex(); - } - - await this.stateEventRunnerService.handleEvent("logout", currentUserId as UserId); + await this.stateEventRunnerService.handleEvent("logout", userId); if (newActiveUser != null) { // we have a new active user, do not continue tearing down application @@ -1183,7 +1240,7 @@ export default class MainBackground { BrowserApi.sendMessage("updateBadge"); } await this.refreshBadge(); - await this.mainContextMenuHandler.noAccess(); + await this.mainContextMenuHandler?.noAccess(); // 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.notificationsService.updateConnection(false); @@ -1238,18 +1295,8 @@ export default class MainBackground { return; } - const getStorage = (): Promise => - new Promise((resolve) => { - chrome.storage.local.get(null, (o: any) => resolve(o)); - }); - - const clearStorage = (): Promise => - new Promise((resolve) => { - chrome.storage.local.clear(() => resolve()); - }); - - const storage = await getStorage(); - await clearStorage(); + const storage = await this.storageService.getAll(); + await this.storageService.clear(); for (const key in storage) { // eslint-disable-next-line diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index 240fb1dede..5ac9961147 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -1,6 +1,8 @@ import { firstValueFrom } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -71,6 +73,8 @@ export class NativeMessagingBackground { private validatingFingerprint: boolean; constructor( + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private cryptoFunctionService: CryptoFunctionService, private runtimeBackground: RuntimeBackground, @@ -200,6 +204,8 @@ export class NativeMessagingBackground { this.privateKey = null; this.connected = false; + this.logService.error("NativeMessaging port disconnected because of error: " + error); + const reason = error != null ? "desktopIntegrationDisabled" : null; reject(new Error(reason)); }); @@ -336,10 +342,14 @@ export class NativeMessagingBackground { ) as UserKey; await this.cryptoService.setUserKey(userKey); } else if (message.keyB64) { + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; // Backwards compatibility to support cases in which the user hasn't updated their desktop app // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3472) - let encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey(); - encUserKey ||= await this.stateService.getMasterKeyEncryptedUserKey(); + const encUserKeyPrim = await this.stateService.getEncryptedCryptoSymmetricKey(); + const encUserKey = + encUserKeyPrim != null + ? new EncString(encUserKeyPrim) + : await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId); if (!encUserKey) { throw new Error("No encrypted user key found"); } @@ -348,9 +358,9 @@ export class NativeMessagingBackground { ) as MasterKey; const userKey = await this.cryptoService.decryptUserKeyWithMasterKey( masterKey, - new EncString(encUserKey), + encUserKey, ); - await this.cryptoService.setMasterKey(masterKey); + await this.masterPasswordService.setMasterKey(masterKey, userId); await this.cryptoService.setUserKey(userKey); } else { throw new Error("No key received"); @@ -389,7 +399,7 @@ export class NativeMessagingBackground { // 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.runtimeBackground.processMessage({ command: "unlocked" }, null); + this.runtimeBackground.processMessage({ command: "unlocked" }); } break; } diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index a88bc051d8..f457889e96 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -1,16 +1,16 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, mergeMap } from "rxjs"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; 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"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { SystemService } from "@bitwarden/common/platform/abstractions/system.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { MessageListener } from "../../../../libs/common/src/platform/messaging"; import { closeUnlockPopout, openSsoAuthResultPopout, @@ -22,8 +22,7 @@ import { BrowserApi } from "../platform/browser/browser-api"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; -import { AbortManager } from "../vault/background/abort-manager"; -import { Fido2Service } from "../vault/services/abstractions/fido2.service"; +import { Fido2Background } from "../vault/fido2/background/abstractions/fido2.background"; import MainBackground from "./main.background"; @@ -32,13 +31,11 @@ export default class RuntimeBackground { private pageDetailsToAutoFill: any[] = []; private onInstalledReason: string = null; private lockedVaultPendingNotifications: LockedVaultPendingNotificationsData[] = []; - private abortManager = new AbortManager(); constructor( private main: MainBackground, private autofillService: AutofillService, private platformUtilsService: BrowserPlatformUtilsService, - private i18nService: I18nService, private notificationsService: NotificationsService, private stateService: BrowserStateService, private autofillSettingsService: AutofillSettingsServiceAbstraction, @@ -47,7 +44,8 @@ export default class RuntimeBackground { private messagingService: MessagingService, private logService: LogService, private configService: ConfigService, - private fido2Service: Fido2Service, + private fido2Background: Fido2Background, + private messageListener: MessageListener, ) { // onInstalled listener must be wired up before anything else, so we do it in the ctor chrome.runtime.onInstalled.addListener((details: any) => { @@ -64,100 +62,47 @@ export default class RuntimeBackground { const backgroundMessageListener = ( msg: any, sender: chrome.runtime.MessageSender, - sendResponse: any, + sendResponse: (response: any) => void, ) => { - const messagesWithResponse = [ - "checkFido2FeatureEnabled", - "fido2RegisterCredentialRequest", - "fido2GetCredentialRequest", - "biometricUnlock", - ]; + const messagesWithResponse = ["biometricUnlock"]; if (messagesWithResponse.includes(msg.command)) { - this.processMessage(msg, sender).then( + this.processMessageWithSender(msg, sender).then( (value) => sendResponse({ result: value }), (error) => sendResponse({ error: { ...error, message: error.message } }), ); return 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 - this.processMessage(msg, sender); + void this.processMessageWithSender(msg, sender).catch((err) => + this.logService.error( + `Error while processing message in RuntimeBackground '${msg?.command}'. Error: ${err?.message ?? "Unknown Error"}`, + ), + ); return false; }; + this.messageListener.allMessages$ + .pipe( + mergeMap(async (message: any) => { + await this.processMessage(message); + }), + ) + .subscribe(); + + // For messages that require the full on message interface BrowserApi.messageListener("runtime.background", backgroundMessageListener); - if (this.main.popupOnlyContext) { - (self as any).bitwardenBackgroundMessageListener = backgroundMessageListener; - } } - async processMessage(msg: any, sender: chrome.runtime.MessageSender) { + // Messages that need the chrome sender and send back a response need to be registered in this method. + async processMessageWithSender(msg: any, sender: chrome.runtime.MessageSender) { switch (msg.command) { - case "loggedIn": - case "unlocked": { - let item: LockedVaultPendingNotificationsData; - - if (msg.command === "loggedIn") { - await this.sendBwInstalledMessageToVault(); - } - - if (this.lockedVaultPendingNotifications?.length > 0) { - item = this.lockedVaultPendingNotifications.pop(); - await closeUnlockPopout(); - } - - await this.notificationsService.updateConnection(msg.command === "loggedIn"); - await this.main.refreshBadge(); - await this.main.refreshMenu(false); - this.systemService.cancelProcessReload(); - - if (item) { - await BrowserApi.focusWindow(item.commandToRetry.sender.tab.windowId); - await BrowserApi.focusTab(item.commandToRetry.sender.tab.id); - await BrowserApi.tabSendMessageData( - item.commandToRetry.sender.tab, - "unlockCompleted", - item, - ); - } - break; - } - case "addToLockedVaultPendingNotifications": - this.lockedVaultPendingNotifications.push(msg.data); - break; - case "logout": - await this.main.logout(msg.expired, msg.userId); - break; - case "syncCompleted": - if (msg.successfully) { - setTimeout(async () => { - await this.main.refreshBadge(); - await this.main.refreshMenu(); - }, 2000); - await this.configService.ensureConfigFetched(); - } - break; - case "openPopup": - await this.main.openPopup(); - break; case "triggerAutofillScriptInjection": await this.autofillService.injectAutofillScripts(sender.tab, sender.frameId); break; case "bgCollectPageDetails": await this.main.collectPageDetailsForContentScript(sender.tab, msg.sender, sender.frameId); break; - case "bgUpdateContextMenu": - case "editedCipher": - case "addedCipher": - case "deletedCipher": - await this.main.refreshBadge(); - await this.main.refreshMenu(); - break; - case "bgReseedStorage": - await this.main.reseedStorage(); - break; case "collectPageDetailsResponse": switch (msg.sender) { case "autofiller": @@ -221,6 +166,72 @@ export default class RuntimeBackground { break; } break; + case "biometricUnlock": { + const result = await this.main.biometricUnlock(); + return result; + } + } + } + + async processMessage(msg: any) { + switch (msg.command) { + case "loggedIn": + case "unlocked": { + let item: LockedVaultPendingNotificationsData; + + if (msg.command === "loggedIn") { + await this.sendBwInstalledMessageToVault(); + } + + if (this.lockedVaultPendingNotifications?.length > 0) { + item = this.lockedVaultPendingNotifications.pop(); + await closeUnlockPopout(); + } + + await this.notificationsService.updateConnection(msg.command === "loggedIn"); + await this.main.refreshBadge(); + await this.main.refreshMenu(false); + this.systemService.cancelProcessReload(); + + if (item) { + await BrowserApi.focusWindow(item.commandToRetry.sender.tab.windowId); + await BrowserApi.focusTab(item.commandToRetry.sender.tab.id); + await BrowserApi.tabSendMessageData( + item.commandToRetry.sender.tab, + "unlockCompleted", + item, + ); + } + break; + } + case "addToLockedVaultPendingNotifications": + this.lockedVaultPendingNotifications.push(msg.data); + break; + case "logout": + await this.main.logout(msg.expired, msg.userId); + break; + case "syncCompleted": + if (msg.successfully) { + setTimeout(async () => { + await this.main.refreshBadge(); + await this.main.refreshMenu(); + }, 2000); + await this.configService.ensureConfigFetched(); + } + break; + case "openPopup": + await this.main.openPopup(); + break; + case "bgUpdateContextMenu": + case "editedCipher": + case "addedCipher": + case "deletedCipher": + await this.main.refreshBadge(); + await this.main.refreshMenu(); + break; + case "bgReseedStorage": + await this.main.reseedStorage(); + break; case "authResult": { const env = await firstValueFrom(this.environmentService.environment$); const vaultUrl = env.getWebVaultUrl(); @@ -269,46 +280,6 @@ export default class RuntimeBackground { case "getClickedElementResponse": this.platformUtilsService.copyToClipboard(msg.identifier); break; - case "triggerFido2ContentScriptInjection": - await this.fido2Service.injectFido2ContentScripts(sender); - break; - case "fido2AbortRequest": - this.abortManager.abort(msg.abortedRequestId); - break; - case "checkFido2FeatureEnabled": - return await this.main.fido2ClientService.isFido2FeatureEnabled(msg.hostname, msg.origin); - case "fido2RegisterCredentialRequest": - return await this.abortManager.runWithAbortController( - msg.requestId, - async (abortController) => { - try { - return await this.main.fido2ClientService.createCredential( - msg.data, - sender.tab, - abortController, - ); - } finally { - await BrowserApi.focusTab(sender.tab.id); - await BrowserApi.focusWindow(sender.tab.windowId); - } - }, - ); - case "fido2GetCredentialRequest": - return await this.abortManager.runWithAbortController( - msg.requestId, - async (abortController) => { - try { - return await this.main.fido2ClientService.assertCredential( - msg.data, - sender.tab, - abortController, - ); - } finally { - await BrowserApi.focusTab(sender.tab.id); - await BrowserApi.focusWindow(sender.tab.windowId); - } - }, - ); case "switchAccount": { await this.main.switchAccount(msg.userId); break; @@ -317,9 +288,6 @@ export default class RuntimeBackground { await this.main.clearClipboard(msg.clipboardValue, msg.timeoutMs); break; } - case "biometricUnlock": { - return await this.main.biometricUnlock(); - } } } @@ -343,9 +311,8 @@ export default class RuntimeBackground { private async checkOnInstalled() { setTimeout(async () => { - // 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.autofillService.loadAutofillScriptsOnInstall(); + void this.fido2Background.injectFido2ContentScriptsInAllTabs(); + void this.autofillService.loadAutofillScriptsOnInstall(); if (this.onInstalledReason != null) { if (this.onInstalledReason === "install") { diff --git a/apps/browser/src/background/service-factories/event-collection-service.factory.ts b/apps/browser/src/background/service-factories/event-collection-service.factory.ts index ec892c73dd..b8f89c90bd 100644 --- a/apps/browser/src/background/service-factories/event-collection-service.factory.ts +++ b/apps/browser/src/background/service-factories/event-collection-service.factory.ts @@ -5,7 +5,10 @@ import { organizationServiceFactory, OrganizationServiceInitOptions, } from "../../admin-console/background/service-factories/organization-service.factory"; -import { accountServiceFactory } from "../../auth/background/service-factories/account-service.factory"; +import { + authServiceFactory, + AuthServiceInitOptions, +} from "../../auth/background/service-factories/auth-service.factory"; import { FactoryOptions, CachedServices, @@ -29,7 +32,8 @@ export type EventCollectionServiceInitOptions = EventCollectionServiceOptions & CipherServiceInitOptions & StateServiceInitOptions & OrganizationServiceInitOptions & - EventUploadServiceInitOptions; + EventUploadServiceInitOptions & + AuthServiceInitOptions; export function eventCollectionServiceFactory( cache: { eventCollectionService?: AbstractEventCollectionService } & CachedServices, @@ -45,7 +49,7 @@ export function eventCollectionServiceFactory( await stateProviderFactory(cache, opts), await organizationServiceFactory(cache, opts), await eventUploadServiceFactory(cache, opts), - await accountServiceFactory(cache, opts), + await authServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/background/service-factories/event-upload-service.factory.ts b/apps/browser/src/background/service-factories/event-upload-service.factory.ts index 4e1d7949be..b20310e8c9 100644 --- a/apps/browser/src/background/service-factories/event-upload-service.factory.ts +++ b/apps/browser/src/background/service-factories/event-upload-service.factory.ts @@ -1,7 +1,10 @@ import { EventUploadService as AbstractEventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; -import { accountServiceFactory } from "../../auth/background/service-factories/account-service.factory"; +import { + AuthServiceInitOptions, + authServiceFactory, +} from "../../auth/background/service-factories/auth-service.factory"; import { ApiServiceInitOptions, apiServiceFactory, @@ -23,7 +26,8 @@ type EventUploadServiceOptions = FactoryOptions; export type EventUploadServiceInitOptions = EventUploadServiceOptions & ApiServiceInitOptions & StateServiceInitOptions & - LogServiceInitOptions; + LogServiceInitOptions & + AuthServiceInitOptions; export function eventUploadServiceFactory( cache: { eventUploadService?: AbstractEventUploadService } & CachedServices, @@ -38,7 +42,7 @@ export function eventUploadServiceFactory( await apiServiceFactory(cache, opts), await stateProviderFactory(cache, opts), await logServiceFactory(cache, opts), - await accountServiceFactory(cache, opts), + await authServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/background/service-factories/search-service.factory.ts b/apps/browser/src/background/service-factories/search-service.factory.ts index 38c7620b5a..aa83d2afd2 100644 --- a/apps/browser/src/background/service-factories/search-service.factory.ts +++ b/apps/browser/src/background/service-factories/search-service.factory.ts @@ -14,12 +14,17 @@ import { logServiceFactory, LogServiceInitOptions, } from "../../platform/background/service-factories/log-service.factory"; +import { + stateProviderFactory, + StateProviderInitOptions, +} from "../../platform/background/service-factories/state-provider.factory"; type SearchServiceFactoryOptions = FactoryOptions; export type SearchServiceInitOptions = SearchServiceFactoryOptions & LogServiceInitOptions & - I18nServiceInitOptions; + I18nServiceInitOptions & + StateProviderInitOptions; export function searchServiceFactory( cache: { searchService?: AbstractSearchService } & CachedServices, @@ -33,6 +38,7 @@ export function searchServiceFactory( new SearchService( await logServiceFactory(cache, opts), await i18nServiceFactory(cache, opts), + await stateProviderFactory(cache, opts), ), ); } diff --git a/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts b/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts index 0e4d1420da..14f055114b 100644 --- a/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts +++ b/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts @@ -1,9 +1,17 @@ import { VaultTimeoutService as AbstractVaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; +import { + accountServiceFactory, + AccountServiceInitOptions, +} from "../../auth/background/service-factories/account-service.factory"; import { authServiceFactory, AuthServiceInitOptions, } from "../../auth/background/service-factories/auth-service.factory"; +import { + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "../../auth/background/service-factories/master-password-service.factory"; import { CryptoServiceInitOptions, cryptoServiceFactory, @@ -57,6 +65,8 @@ type VaultTimeoutServiceFactoryOptions = FactoryOptions & { }; export type VaultTimeoutServiceInitOptions = VaultTimeoutServiceFactoryOptions & + AccountServiceInitOptions & + MasterPasswordServiceInitOptions & CipherServiceInitOptions & FolderServiceInitOptions & CollectionServiceInitOptions & @@ -79,6 +89,8 @@ export function vaultTimeoutServiceFactory( opts, async () => new VaultTimeoutService( + await accountServiceFactory(cache, opts), + await internalMasterPasswordServiceFactory(cache, opts), await cipherServiceFactory(cache, opts), await folderServiceFactory(cache, opts), await collectionServiceFactory(cache, opts), diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 271b2c76a2..78f1e2cc41 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.3.1", + "version": "2024.4.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", @@ -22,13 +22,6 @@ "exclude_matches": ["*://*/*.xml*", "file:///*.xml*"], "run_at": "document_start" }, - { - "all_frames": true, - "js": ["content/fido2/trigger-fido2-content-script-injection.js"], - "matches": ["https://*/*"], - "exclude_matches": ["https://*/*.xml*"], - "run_at": "document_start" - }, { "all_frames": true, "css": ["content/autofill.css"], @@ -67,7 +60,8 @@ "clipboardWrite", "idle", "webRequest", - "webRequestBlocking" + "webRequestBlocking", + "webNavigation" ], "optional_permissions": ["nativeMessaging", "privacy"], "content_security_policy": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index e7b0c0cd1e..cdd0869fc5 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.3.1", + "version": "2024.4.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", @@ -23,13 +23,6 @@ "exclude_matches": ["*://*/*.xml*", "file:///*.xml*"], "run_at": "document_start" }, - { - "all_frames": true, - "js": ["content/fido2/trigger-fido2-content-script-injection.js"], - "matches": ["https://*/*"], - "exclude_matches": ["https://*/*.xml*"], - "run_at": "document_start" - }, { "all_frames": true, "css": ["content/autofill.css"], @@ -71,7 +64,7 @@ "offscreen" ], "optional_permissions": ["nativeMessaging", "privacy"], - "host_permissions": ["*://*/*"], + "host_permissions": [""], "content_security_policy": { "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'", "sandbox": "sandbox allow-scripts; script-src 'self'" diff --git a/apps/browser/src/models/browserSendComponentState.ts b/apps/browser/src/models/browserSendComponentState.ts index 9158efc21d..81dd93323b 100644 --- a/apps/browser/src/models/browserSendComponentState.ts +++ b/apps/browser/src/models/browserSendComponentState.ts @@ -1,5 +1,3 @@ -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { DeepJsonify } from "@bitwarden/common/types/deep-jsonify"; @@ -7,13 +5,6 @@ import { BrowserComponentState } from "./browserComponentState"; export class BrowserSendComponentState extends BrowserComponentState { sends: SendView[]; - typeCounts: Map; - - toJSON() { - return Utils.merge(this, { - typeCounts: Utils.mapToRecord(this.typeCounts), - }); - } static fromJSON(json: DeepJsonify) { if (json == null) { @@ -22,7 +13,6 @@ export class BrowserSendComponentState extends BrowserComponentState { return Object.assign(new BrowserSendComponentState(), json, { sends: json.sends?.map((s) => SendView.fromJSON(s)), - typeCounts: Utils.recordToMap(json.typeCounts), }); } } diff --git a/apps/browser/src/platform/background.ts b/apps/browser/src/platform/background.ts index 9c3510178c..a48c420e77 100644 --- a/apps/browser/src/platform/background.ts +++ b/apps/browser/src/platform/background.ts @@ -5,16 +5,11 @@ 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() - .then(() => { - // Finished bootstrapping - if (BrowserApi.isManifestVersion(3)) { - startHeartbeat().catch((error) => logService.error(error)); - } - }) - .catch((error) => logService.error(error)); +bitwardenMain.bootstrap().catch((error) => logService.error(error)); /** * Tracks when a service worker was last alive and extends the service worker diff --git a/apps/browser/src/platform/background/service-factories/browser-script-injector-service.factory.ts b/apps/browser/src/platform/background/service-factories/browser-script-injector-service.factory.ts new file mode 100644 index 0000000000..e3bc687f28 --- /dev/null +++ b/apps/browser/src/platform/background/service-factories/browser-script-injector-service.factory.ts @@ -0,0 +1,19 @@ +import { BrowserScriptInjectorService } from "../../services/browser-script-injector.service"; + +import { CachedServices, FactoryOptions, factory } from "./factory-options"; + +type BrowserScriptInjectorServiceOptions = FactoryOptions; + +export type BrowserScriptInjectorServiceInitOptions = BrowserScriptInjectorServiceOptions; + +export function browserScriptInjectorServiceFactory( + cache: { browserScriptInjectorService?: BrowserScriptInjectorService } & CachedServices, + opts: BrowserScriptInjectorServiceInitOptions, +): Promise { + return factory( + cache, + "browserScriptInjectorService", + opts, + async () => new BrowserScriptInjectorService(), + ); +} diff --git a/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts b/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts index 97614660d1..ed4fde162c 100644 --- a/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts @@ -4,6 +4,10 @@ import { AccountServiceInitOptions, accountServiceFactory, } from "../../../auth/background/service-factories/account-service.factory"; +import { + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "../../../auth/background/service-factories/master-password-service.factory"; import { StateServiceInitOptions, stateServiceFactory, @@ -34,6 +38,7 @@ import { StateProviderInitOptions, stateProviderFactory } from "./state-provider type CryptoServiceFactoryOptions = FactoryOptions; export type CryptoServiceInitOptions = CryptoServiceFactoryOptions & + MasterPasswordServiceInitOptions & KeyGenerationServiceInitOptions & CryptoFunctionServiceInitOptions & EncryptServiceInitOptions & @@ -53,6 +58,7 @@ export function cryptoServiceFactory( opts, async () => new BrowserCryptoService( + await internalMasterPasswordServiceFactory(cache, opts), await keyGenerationServiceFactory(cache, opts), await cryptoFunctionServiceFactory(cache, opts), await encryptServiceFactory(cache, opts), diff --git a/apps/browser/src/platform/background/service-factories/message-sender.factory.ts b/apps/browser/src/platform/background/service-factories/message-sender.factory.ts new file mode 100644 index 0000000000..6f50b4b8f5 --- /dev/null +++ b/apps/browser/src/platform/background/service-factories/message-sender.factory.ts @@ -0,0 +1,17 @@ +import { MessageSender } from "@bitwarden/common/platform/messaging"; + +import { CachedServices, factory, FactoryOptions } from "./factory-options"; + +type MessagingServiceFactoryOptions = FactoryOptions; + +export type MessageSenderInitOptions = MessagingServiceFactoryOptions; + +export function messageSenderFactory( + cache: { messagingService?: MessageSender } & CachedServices, + opts: MessageSenderInitOptions, +): Promise { + // NOTE: Name needs to match that of MainBackground property until we delete these. + return factory(cache, "messagingService", opts, () => { + throw new Error("Not implemented, not expected to be used."); + }); +} diff --git a/apps/browser/src/platform/background/service-factories/messaging-service.factory.ts b/apps/browser/src/platform/background/service-factories/messaging-service.factory.ts index 46852712aa..20c6e3f424 100644 --- a/apps/browser/src/platform/background/service-factories/messaging-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/messaging-service.factory.ts @@ -1,19 +1,5 @@ -import { MessagingService as AbstractMessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -import { - CachedServices, - factory, - FactoryOptions, -} from "../../background/service-factories/factory-options"; -import BrowserMessagingService from "../../services/browser-messaging.service"; - -type MessagingServiceFactoryOptions = FactoryOptions; - -export type MessagingServiceInitOptions = MessagingServiceFactoryOptions; - -export function messagingServiceFactory( - cache: { messagingService?: AbstractMessagingService } & CachedServices, - opts: MessagingServiceInitOptions, -): Promise { - return factory(cache, "messagingService", opts, () => new BrowserMessagingService()); -} +// Export old messaging service stuff to minimize changes +export { + messageSenderFactory as messagingServiceFactory, + MessageSenderInitOptions as MessagingServiceInitOptions, +} from "./message-sender.factory"; diff --git a/apps/browser/src/platform/background/service-factories/state-service.factory.ts b/apps/browser/src/platform/background/service-factories/state-service.factory.ts index 20a9ac074a..5567e00990 100644 --- a/apps/browser/src/platform/background/service-factories/state-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/state-service.factory.ts @@ -10,7 +10,7 @@ import { TokenServiceInitOptions, } from "../../../auth/background/service-factories/token-service.factory"; import { Account } from "../../../models/account"; -import { BrowserStateService } from "../../services/browser-state.service"; +import { DefaultBrowserStateService } from "../../services/default-browser-state.service"; import { environmentServiceFactory, @@ -46,15 +46,15 @@ export type StateServiceInitOptions = StateServiceFactoryOptions & MigrationRunnerInitOptions; export async function stateServiceFactory( - cache: { stateService?: BrowserStateService } & CachedServices, + cache: { stateService?: DefaultBrowserStateService } & CachedServices, opts: StateServiceInitOptions, -): Promise { +): Promise { const service = await factory( cache, "stateService", opts, async () => - new BrowserStateService( + new DefaultBrowserStateService( await diskStorageServiceFactory(cache, opts), await secureStorageServiceFactory(cache, opts), await memoryStorageServiceFactory(cache, opts), diff --git a/apps/browser/src/platform/background/service-factories/storage-service.factory.ts b/apps/browser/src/platform/background/service-factories/storage-service.factory.ts index 19d5a9c140..83e8a780a6 100644 --- a/apps/browser/src/platform/background/service-factories/storage-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/storage-service.factory.ts @@ -17,6 +17,11 @@ import { KeyGenerationServiceInitOptions, keyGenerationServiceFactory, } from "./key-generation-service.factory"; +import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory"; +import { + platformUtilsServiceFactory, + PlatformUtilsServiceInitOptions, +} from "./platform-utils-service.factory"; export type DiskStorageServiceInitOptions = FactoryOptions; export type SecureStorageServiceInitOptions = FactoryOptions; @@ -25,7 +30,9 @@ export type MemoryStorageServiceInitOptions = FactoryOptions & EncryptServiceInitOptions & KeyGenerationServiceInitOptions & DiskStorageServiceInitOptions & - SessionStorageServiceInitOptions; + SessionStorageServiceInitOptions & + LogServiceInitOptions & + PlatformUtilsServiceInitOptions; export function diskStorageServiceFactory( cache: { diskStorageService?: AbstractStorageService } & CachedServices, @@ -63,10 +70,12 @@ export function memoryStorageServiceFactory( return factory(cache, "memoryStorageService", opts, async () => { if (BrowserApi.isManifestVersion(3)) { return new LocalBackedSessionStorageService( + await logServiceFactory(cache, opts), await encryptServiceFactory(cache, opts), await keyGenerationServiceFactory(cache, opts), await diskStorageServiceFactory(cache, opts), await sessionStorageServiceFactory(cache, opts), + await platformUtilsServiceFactory(cache, opts), "serviceFactories", ); } diff --git a/apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts b/apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts new file mode 100644 index 0000000000..8a20f3e999 --- /dev/null +++ b/apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts @@ -0,0 +1,435 @@ +/** + * MIT License + * + * Copyright (c) Federico Brigante (https://fregante.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @see https://github.com/fregante/content-scripts-register-polyfill + * @version 4.0.2 + */ +import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; + +import { BrowserApi } from "./browser-api"; + +let registerContentScripts: ( + contentScriptOptions: browser.contentScripts.RegisteredContentScriptOptions, + callback?: (registeredContentScript: browser.contentScripts.RegisteredContentScript) => void, +) => Promise; +export async function registerContentScriptsPolyfill( + contentScriptOptions: browser.contentScripts.RegisteredContentScriptOptions, + callback?: (registeredContentScript: browser.contentScripts.RegisteredContentScript) => void, +) { + if (!registerContentScripts) { + registerContentScripts = buildRegisterContentScriptsPolyfill(); + } + + return registerContentScripts(contentScriptOptions, callback); +} + +function buildRegisterContentScriptsPolyfill() { + const logService = new ConsoleLogService(false); + const chromeProxy = globalThis.chrome && NestedProxy(globalThis.chrome); + const patternValidationRegex = + /^(https?|wss?|file|ftp|\*):\/\/(\*|\*\.[^*/]+|[^*/]+)\/.*$|^file:\/\/\/.*$|^resource:\/\/(\*|\*\.[^*/]+|[^*/]+)\/.*$|^about:/; + const isFirefox = globalThis.navigator?.userAgent.includes("Firefox/"); + const gotScripting = Boolean(globalThis.chrome?.scripting); + const gotNavigation = typeof chrome === "object" && "webNavigation" in chrome; + + function NestedProxy(target: T): T { + return new Proxy(target, { + get(target, prop) { + if (!target[prop as keyof T]) { + return; + } + + if (typeof target[prop as keyof T] !== "function") { + return NestedProxy(target[prop as keyof T]); + } + + return (...arguments_: any[]) => + new Promise((resolve, reject) => { + target[prop as keyof T](...arguments_, (result: any) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(result); + } + }); + }); + }, + }); + } + + function assertValidPattern(matchPattern: string) { + if (!isValidPattern(matchPattern)) { + throw new Error( + `${matchPattern} is an invalid pattern, it must match ${String(patternValidationRegex)}`, + ); + } + } + + function isValidPattern(matchPattern: string) { + return matchPattern === "" || patternValidationRegex.test(matchPattern); + } + + function getRawPatternRegex(matchPattern: string) { + assertValidPattern(matchPattern); + let [, protocol, host = "", pathname] = matchPattern.split(/(^[^:]+:[/][/])([^/]+)?/); + protocol = protocol + .replace("*", isFirefox ? "(https?|wss?)" : "https?") + .replaceAll(/[/]/g, "[/]"); + + if (host === "*") { + host = "[^/]+"; + } else if (host) { + host = host + .replace(/^[*][.]/, "([^/]+.)*") + .replaceAll(/[.]/g, "[.]") + .replace(/[*]$/, "[^.]+"); + } + + pathname = pathname + .replaceAll(/[/]/g, "[/]") + .replaceAll(/[.]/g, "[.]") + .replaceAll(/[*]/g, ".*"); + + return "^" + protocol + host + "(" + pathname + ")?$"; + } + + function patternToRegex(...matchPatterns: string[]) { + if (matchPatterns.length === 0) { + return /$./; + } + + if (matchPatterns.includes("")) { + // regex + return /^(https?|file|ftp):[/]+/; + } + + if (matchPatterns.includes("*://*/*")) { + // all stars regex + return isFirefox ? /^(https?|wss?):[/][/][^/]+([/].*)?$/ : /^https?:[/][/][^/]+([/].*)?$/; + } + + return new RegExp(matchPatterns.map((x) => getRawPatternRegex(x)).join("|")); + } + + function castAllFramesTarget(target: number | { tabId: number; frameId: number }) { + if (typeof target === "object") { + return { ...target, allFrames: false }; + } + + return { + tabId: target, + frameId: undefined, + allFrames: true, + }; + } + + function castArray(possibleArray: any | any[]) { + if (Array.isArray(possibleArray)) { + return possibleArray; + } + + return [possibleArray]; + } + + function arrayOrUndefined(value?: number) { + return value === undefined ? undefined : [value]; + } + + async function insertCSS( + { + tabId, + frameId, + files, + allFrames, + matchAboutBlank, + runAt, + }: { + tabId: number; + frameId?: number; + files: browser.extensionTypes.ExtensionFileOrCode[]; + allFrames: boolean; + matchAboutBlank: boolean; + runAt: browser.extensionTypes.RunAt; + }, + { ignoreTargetErrors }: { ignoreTargetErrors?: boolean } = {}, + ) { + const everyInsertion = Promise.all( + files.map(async (content) => { + if (typeof content === "string") { + content = { file: content }; + } + + if (gotScripting) { + return chrome.scripting.insertCSS({ + target: { + tabId, + frameIds: arrayOrUndefined(frameId), + allFrames: frameId === undefined ? allFrames : undefined, + }, + files: "file" in content ? [content.file] : undefined, + css: "code" in content ? content.code : undefined, + }); + } + + return chromeProxy.tabs.insertCSS(tabId, { + ...content, + matchAboutBlank, + allFrames, + frameId, + runAt: runAt ?? "document_start", + }); + }), + ); + + if (ignoreTargetErrors) { + await catchTargetInjectionErrors(everyInsertion); + } else { + await everyInsertion; + } + } + function assertNoCode(files: browser.extensionTypes.ExtensionFileOrCode[]) { + if (files.some((content) => "code" in content)) { + throw new Error("chrome.scripting does not support injecting strings of `code`"); + } + } + + async function executeScript( + { + tabId, + frameId, + files, + allFrames, + matchAboutBlank, + runAt, + }: { + tabId: number; + frameId?: number; + files: browser.extensionTypes.ExtensionFileOrCode[]; + allFrames: boolean; + matchAboutBlank: boolean; + runAt: browser.extensionTypes.RunAt; + }, + { ignoreTargetErrors }: { ignoreTargetErrors?: boolean } = {}, + ) { + const normalizedFiles = files.map((file) => (typeof file === "string" ? { file } : file)); + + if (gotScripting) { + assertNoCode(normalizedFiles); + const injection = chrome.scripting.executeScript({ + target: { + tabId, + frameIds: arrayOrUndefined(frameId), + allFrames: frameId === undefined ? allFrames : undefined, + }, + files: normalizedFiles.map(({ file }: { file: string }) => file), + }); + + if (ignoreTargetErrors) { + await catchTargetInjectionErrors(injection); + } else { + await injection; + } + + return; + } + + const executions = []; + for (const content of normalizedFiles) { + if ("code" in content) { + await executions.at(-1); + } + + executions.push( + chromeProxy.tabs.executeScript(tabId, { + ...content, + matchAboutBlank, + allFrames, + frameId, + runAt, + }), + ); + } + + if (ignoreTargetErrors) { + await catchTargetInjectionErrors(Promise.all(executions)); + } else { + await Promise.all(executions); + } + } + + async function injectContentScript( + where: { tabId: number; frameId: number }, + scripts: { + css: browser.extensionTypes.ExtensionFileOrCode[]; + js: browser.extensionTypes.ExtensionFileOrCode[]; + matchAboutBlank: boolean; + runAt: browser.extensionTypes.RunAt; + }, + options = {}, + ) { + const targets = castArray(where); + await Promise.all( + targets.map(async (target) => + injectContentScriptInSpecificTarget(castAllFramesTarget(target), scripts, options), + ), + ); + } + + async function injectContentScriptInSpecificTarget( + { frameId, tabId, allFrames }: { frameId?: number; tabId: number; allFrames: boolean }, + scripts: { + css: browser.extensionTypes.ExtensionFileOrCode[]; + js: browser.extensionTypes.ExtensionFileOrCode[]; + matchAboutBlank: boolean; + runAt: browser.extensionTypes.RunAt; + }, + options = {}, + ) { + const injections = castArray(scripts).flatMap((script) => [ + insertCSS( + { + tabId, + frameId, + allFrames, + files: script.css ?? [], + matchAboutBlank: script.matchAboutBlank ?? script.match_about_blank, + runAt: script.runAt ?? script.run_at, + }, + options, + ), + executeScript( + { + tabId, + frameId, + allFrames, + files: script.js ?? [], + matchAboutBlank: script.matchAboutBlank ?? script.match_about_blank, + runAt: script.runAt ?? script.run_at, + }, + options, + ), + ]); + await Promise.all(injections); + } + + async function catchTargetInjectionErrors(promise: Promise) { + try { + await promise; + } catch (error) { + const targetErrors = + /^No frame with id \d+ in tab \d+.$|^No tab with id: \d+.$|^The tab was closed.$|^The frame was removed.$/; + if (!targetErrors.test(error?.message)) { + throw error; + } + } + } + + async function isOriginPermitted(url: string) { + return chromeProxy.permissions.contains({ + origins: [new URL(url).origin + "/*"], + }); + } + + return async ( + contentScriptOptions: browser.contentScripts.RegisteredContentScriptOptions, + callback: CallableFunction, + ) => { + const { + js = [], + css = [], + matchAboutBlank, + matches = [], + excludeMatches, + runAt, + } = contentScriptOptions; + let { allFrames } = contentScriptOptions; + + if (gotNavigation) { + allFrames = false; + } else if (allFrames) { + logService.warning( + "`allFrames: true` requires the `webNavigation` permission to work correctly: https://github.com/fregante/content-scripts-register-polyfill#permissions", + ); + } + + if (matches.length === 0) { + throw new Error( + "Type error for parameter contentScriptOptions (Error processing matches: Array requires at least 1 items; you have 0) for contentScripts.register.", + ); + } + + await Promise.all( + matches.map(async (pattern: string) => { + if (!(await chromeProxy.permissions.contains({ origins: [pattern] }))) { + throw new Error(`Permission denied to register a content script for ${pattern}`); + } + }), + ); + + const matchesRegex = patternToRegex(...matches); + const excludeMatchesRegex = patternToRegex( + ...(excludeMatches !== null && excludeMatches !== void 0 ? excludeMatches : []), + ); + const inject = async (url: string, tabId: number, frameId = 0) => { + if ( + !matchesRegex.test(url) || + excludeMatchesRegex.test(url) || + !(await isOriginPermitted(url)) + ) { + return; + } + + await injectContentScript( + { tabId, frameId }, + { css, js, matchAboutBlank, runAt }, + { ignoreTargetErrors: true }, + ); + }; + const tabListener = async ( + tabId: number, + { status }: chrome.tabs.TabChangeInfo, + { url }: chrome.tabs.Tab, + ) => { + if (status === "loading" && url) { + void inject(url, tabId); + } + }; + const navListener = async ({ + tabId, + frameId, + url, + }: chrome.webNavigation.WebNavigationTransitionCallbackDetails) => { + void inject(url, tabId, frameId); + }; + + if (gotNavigation) { + BrowserApi.addListener(chrome.webNavigation.onCommitted, navListener); + } else { + BrowserApi.addListener(chrome.tabs.onUpdated, tabListener); + } + + const registeredContentScript = { + async unregister() { + if (gotNavigation) { + chrome.webNavigation.onCommitted.removeListener(navListener); + } else { + chrome.tabs.onUpdated.removeListener(tabListener); + } + }, + }; + + if (typeof callback === "function") { + callback(registeredContentScript); + } + + return registeredContentScript; + }; +} diff --git a/apps/browser/src/platform/browser/browser-api.spec.ts b/apps/browser/src/platform/browser/browser-api.spec.ts index a1dafb38ec..e452d6d8ee 100644 --- a/apps/browser/src/platform/browser/browser-api.spec.ts +++ b/apps/browser/src/platform/browser/browser-api.spec.ts @@ -550,4 +550,35 @@ describe("BrowserApi", () => { expect(callbackMock).toHaveBeenCalled(); }); }); + + describe("registerContentScriptsMv2", () => { + const details: browser.contentScripts.RegisteredContentScriptOptions = { + matches: [""], + js: [{ file: "content/fido2/page-script.js" }], + }; + + it("registers content scripts through the `browser.contentScripts` API when the API is available", async () => { + globalThis.browser = mock({ + contentScripts: { register: jest.fn() }, + }); + + await BrowserApi.registerContentScriptsMv2(details); + + expect(browser.contentScripts.register).toHaveBeenCalledWith(details); + }); + + it("registers content scripts through the `registerContentScriptsPolyfill` when the `browser.contentScripts.register` API is not available", async () => { + globalThis.browser = mock({ + contentScripts: { register: undefined }, + }); + jest.spyOn(BrowserApi, "addListener"); + + await BrowserApi.registerContentScriptsMv2(details); + + expect(BrowserApi.addListener).toHaveBeenCalledWith( + chrome.webNavigation.onCommitted, + expect.any(Function), + ); + }); + }); }); diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index b2ee66f051..b793777d8b 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -5,6 +5,8 @@ import { DeviceType } from "@bitwarden/common/enums"; import { TabMessage } from "../../types/tab-messages"; import { BrowserPlatformUtilsService } from "../services/platform-utils/browser-platform-utils.service"; +import { registerContentScriptsPolyfill } from "./browser-api.register-content-scripts-polyfill"; + export class BrowserApi { static isWebExtensionsApi: boolean = typeof browser !== "undefined"; static isSafariApi: boolean = @@ -591,4 +593,41 @@ export class BrowserApi { } }); } + + /** + * Handles registration of static content scripts within manifest v2. + * + * @param contentScriptOptions - Details of the registered content scripts + */ + static async registerContentScriptsMv2( + contentScriptOptions: browser.contentScripts.RegisteredContentScriptOptions, + ): Promise { + if (typeof browser !== "undefined" && !!browser.contentScripts?.register) { + return await browser.contentScripts.register(contentScriptOptions); + } + + return await registerContentScriptsPolyfill(contentScriptOptions); + } + + /** + * Handles registration of static content scripts within manifest v3. + * + * @param scripts - Details of the registered content scripts + */ + static async registerContentScriptsMv3( + scripts: chrome.scripting.RegisteredContentScript[], + ): Promise { + await chrome.scripting.registerContentScripts(scripts); + } + + /** + * Handles unregistering of static content scripts within manifest v3. + * + * @param filter - Optional filter to unregister content scripts. Passing an empty object will unregister all content scripts. + */ + static async unregisterContentScriptsMv3( + filter?: chrome.scripting.ContentScriptFilter, + ): Promise { + await chrome.scripting.unregisterContentScripts(filter); + } } diff --git a/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.spec.ts b/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.spec.ts index 4b0226d54e..2092f6992b 100644 --- a/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.spec.ts +++ b/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.spec.ts @@ -3,7 +3,7 @@ import { BehaviorSubject } from "rxjs"; import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; -import { BrowserStateService } from "../../services/browser-state.service"; +import { DefaultBrowserStateService } from "../../services/default-browser-state.service"; import { browserSession } from "./browser-session.decorator"; import { SessionStorable } from "./session-storable"; @@ -25,7 +25,7 @@ describe("browserSession decorator", () => { }); it("should create if StateService is a constructor argument", () => { - const stateService = Object.create(BrowserStateService.prototype, { + const stateService = Object.create(DefaultBrowserStateService.prototype, { memoryStorageService: { value: Object.create(MemoryStorageService.prototype, { type: { value: MemoryStorageService.TYPE }, @@ -35,7 +35,7 @@ describe("browserSession decorator", () => { @browserSession class TestClass { - constructor(private stateService: BrowserStateService) {} + constructor(private stateService: DefaultBrowserStateService) {} } expect(new TestClass(stateService)).toBeDefined(); diff --git a/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.ts b/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.ts index 692e33bcce..6561d5074c 100644 --- a/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.ts +++ b/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.ts @@ -93,6 +93,10 @@ export class SessionSyncer { } async update(serializedValue: any) { + if (!serializedValue) { + return; + } + const unBuiltValue = JSON.parse(serializedValue); if (!BrowserApi.isManifestVersion(3) && BrowserApi.isBackgroundPage(self)) { await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue); @@ -104,6 +108,10 @@ export class SessionSyncer { } private async updateSession(value: any) { + if (!value) { + return; + } + const serializedValue = JSON.stringify(value); if (BrowserApi.isManifestVersion(3) || BrowserApi.isBackgroundPage(self)) { await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue); diff --git a/apps/browser/src/platform/flags.ts b/apps/browser/src/platform/flags.ts index 71a20edc5e..36aa698a7b 100644 --- a/apps/browser/src/platform/flags.ts +++ b/apps/browser/src/platform/flags.ts @@ -11,13 +11,13 @@ import { GroupPolicyEnvironment } from "../admin-console/types/group-policy-envi import { BrowserApi } from "./browser/browser-api"; // required to avoid linting errors when there are no flags -/* eslint-disable-next-line @typescript-eslint/ban-types */ +// eslint-disable-next-line @typescript-eslint/ban-types export type Flags = { accountSwitching?: boolean; } & SharedFlags; // required to avoid linting errors when there are no flags -/* eslint-disable-next-line @typescript-eslint/ban-types */ +// eslint-disable-next-line @typescript-eslint/ban-types export type DevFlags = { storeSessionDecrypted?: boolean; managedEnvironment?: GroupPolicyEnvironment; diff --git a/apps/browser/src/platform/listeners/on-install-listener.ts b/apps/browser/src/platform/listeners/on-install-listener.ts index ef206301e3..adf575a17a 100644 --- a/apps/browser/src/platform/listeners/on-install-listener.ts +++ b/apps/browser/src/platform/listeners/on-install-listener.ts @@ -23,6 +23,11 @@ export async function onInstallListener(details: chrome.runtime.InstalledDetails stateServiceOptions: { stateFactory: new StateFactory(GlobalState, Account), }, + platformUtilsServiceOptions: { + win: self, + biometricCallback: async () => false, + clipboardWriteCallback: async () => {}, + }, }; const environmentService = await environmentServiceFactory(cache, opts); diff --git a/apps/browser/src/platform/messaging/chrome-message.sender.ts b/apps/browser/src/platform/messaging/chrome-message.sender.ts new file mode 100644 index 0000000000..0e57ecfb4e --- /dev/null +++ b/apps/browser/src/platform/messaging/chrome-message.sender.ts @@ -0,0 +1,37 @@ +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CommandDefinition, MessageSender } from "@bitwarden/common/platform/messaging"; +import { getCommand } from "@bitwarden/common/platform/messaging/internal"; + +type ErrorHandler = (logger: LogService, command: string) => void; + +const HANDLED_ERRORS: Record = { + "Could not establish connection. Receiving end does not exist.": (logger, command) => + logger.debug(`Receiving end didn't exist for command '${command}'`), + + "The message port closed before a response was received.": (logger, command) => + logger.debug(`Port was closed for command '${command}'`), +}; + +export class ChromeMessageSender implements MessageSender { + constructor(private readonly logService: LogService) {} + + send( + commandDefinition: string | CommandDefinition, + payload: object | T = {}, + ): void { + const command = getCommand(commandDefinition); + chrome.runtime.sendMessage(Object.assign(payload, { command: command }), () => { + if (chrome.runtime.lastError) { + const errorHandler = HANDLED_ERRORS[chrome.runtime.lastError.message]; + if (errorHandler != null) { + errorHandler(this.logService, command); + return; + } + + this.logService.warning( + `Unhandled error while sending message with command '${command}': ${chrome.runtime.lastError.message}`, + ); + } + }); + } +} diff --git a/apps/browser/src/platform/popup/header.component.ts b/apps/browser/src/platform/popup/header.component.ts index 6b9e9c9a3e..ebda12c2a4 100644 --- a/apps/browser/src/platform/popup/header.component.ts +++ b/apps/browser/src/platform/popup/header.component.ts @@ -1,8 +1,10 @@ import { Component, Input } from "@angular/core"; -import { Observable, map } from "rxjs"; +import { Observable, combineLatest, map, of, switchMap } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { UserId } from "@bitwarden/common/types/guid"; import { enableAccountSwitching } from "../flags"; @@ -14,14 +16,18 @@ export class HeaderComponent { @Input() noTheme = false; @Input() hideAccountSwitcher = false; authedAccounts$: Observable; - constructor(accountService: AccountService) { + constructor(accountService: AccountService, authService: AuthService) { this.authedAccounts$ = accountService.accounts$.pipe( - map((accounts) => { + switchMap((accounts) => { if (!enableAccountSwitching()) { - return false; + return of(false); } - return Object.values(accounts).some((a) => a.status !== AuthenticationStatus.LoggedOut); + return combineLatest( + Object.keys(accounts).map((id) => authService.authStatusFor$(id as UserId)), + ).pipe( + map((statuses) => statuses.some((status) => status !== AuthenticationStatus.LoggedOut)), + ); }), ); } diff --git a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts index 22ce8d4564..64935ab591 100644 --- a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts +++ b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts @@ -8,6 +8,25 @@ import { import { fromChromeEvent } from "../../browser/from-chrome-event"; +export const serializationIndicator = "__json__"; + +type serializedObject = { [serializationIndicator]: true; value: string }; + +export const objToStore = (obj: any) => { + if (obj == null) { + return null; + } + + if (obj instanceof Set) { + obj = Array.from(obj); + } + + return { + [serializationIndicator]: true, + value: JSON.stringify(obj), + }; +}; + export default abstract class AbstractChromeStorageService implements AbstractStorageService, ObservableStorageService { @@ -44,7 +63,7 @@ export default abstract class AbstractChromeStorageService return new Promise((resolve) => { this.chromeStorageApi.get(key, (obj: any) => { if (obj != null && obj[key] != null) { - resolve(obj[key] as T); + resolve(this.processGetObject(obj[key])); return; } resolve(null); @@ -57,14 +76,7 @@ export default abstract class AbstractChromeStorageService } async save(key: string, obj: any): Promise { - if (obj == null) { - // Fix safari not liking null in set - return this.remove(key); - } - - if (obj instanceof Set) { - obj = Array.from(obj); - } + obj = objToStore(obj); const keyedObj = { [key]: obj }; return new Promise((resolve) => { @@ -81,4 +93,22 @@ export default abstract class AbstractChromeStorageService }); }); } + + /** Backwards compatible resolution of retrieved object with new serialized storage */ + protected processGetObject(obj: T | serializedObject): T | null { + if (this.isSerialized(obj)) { + obj = JSON.parse(obj.value); + } + return obj as T; + } + + /** Type guard for whether an object is tagged as serialized */ + protected isSerialized(value: T | serializedObject): value is serializedObject { + const asSerialized = value as serializedObject; + return ( + asSerialized != null && + asSerialized[serializationIndicator] && + typeof asSerialized.value === "string" + ); + } } diff --git a/apps/browser/src/platform/services/abstractions/browser-state.service.ts b/apps/browser/src/platform/services/abstractions/browser-state.service.ts index 82ec54975a..c8e2c502e7 100644 --- a/apps/browser/src/platform/services/abstractions/browser-state.service.ts +++ b/apps/browser/src/platform/services/abstractions/browser-state.service.ts @@ -1,19 +1,5 @@ import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; -import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; import { Account } from "../../../models/account"; -import { BrowserComponentState } from "../../../models/browserComponentState"; -import { BrowserSendComponentState } from "../../../models/browserSendComponentState"; -export abstract class BrowserStateService extends BaseStateServiceAbstraction { - getBrowserSendComponentState: (options?: StorageOptions) => Promise; - setBrowserSendComponentState: ( - value: BrowserSendComponentState, - options?: StorageOptions, - ) => Promise; - getBrowserSendTypeComponentState: (options?: StorageOptions) => Promise; - setBrowserSendTypeComponentState: ( - value: BrowserComponentState, - options?: StorageOptions, - ) => Promise; -} +export abstract class BrowserStateService extends BaseStateServiceAbstraction {} diff --git a/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts b/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts new file mode 100644 index 0000000000..812901879d --- /dev/null +++ b/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts @@ -0,0 +1,105 @@ +import AbstractChromeStorageService, { + objToStore, + serializationIndicator, +} from "./abstract-chrome-storage-api.service"; + +class TestChromeStorageApiService extends AbstractChromeStorageService {} + +describe("objectToStore", () => { + it("converts an object to a tagged string", () => { + const obj = { key: "value" }; + const result = objToStore(obj); + expect(result).toEqual({ + [serializationIndicator]: true, + value: JSON.stringify(obj), + }); + }); + + it("converts a set to an array prior to serialization", () => { + const obj = new Set(["value"]); + const result = objToStore(obj); + expect(result).toEqual({ + [serializationIndicator]: true, + value: JSON.stringify(Array.from(obj)), + }); + }); + + it("does nothing to null", () => { + expect(objToStore(null)).toEqual(null); + }); +}); + +describe("ChromeStorageApiService", () => { + let service: TestChromeStorageApiService; + let store: Record; + + beforeEach(() => { + store = {}; + + service = new TestChromeStorageApiService(chrome.storage.local); + }); + + describe("save", () => { + let setMock: jest.Mock; + + beforeEach(() => { + // setup save + setMock = chrome.storage.local.set as jest.Mock; + setMock.mockImplementation((data, callback) => { + Object.assign(store, data); + callback(); + }); + }); + + it("uses `objToStore` to prepare a value for set", async () => { + const key = "key"; + const value = { key: "value" }; + await service.save(key, value); + expect(setMock).toHaveBeenCalledWith( + { + [key]: objToStore(value), + }, + expect.any(Function), + ); + }); + }); + + describe("get", () => { + let getMock: jest.Mock; + const key = "key"; + + beforeEach(() => { + // setup get + getMock = chrome.storage.local.get as jest.Mock; + getMock.mockImplementation((key, callback) => { + callback({ [key]: store[key] }); + }); + }); + + it("returns a stored value when it is serialized", async () => { + const value = { key: "value" }; + store[key] = objToStore(value); + const result = await service.get(key); + expect(result).toEqual(value); + }); + + it("returns a stored value when it is not serialized", async () => { + const value = "value"; + store[key] = value; + const result = await service.get(key); + expect(result).toEqual(value); + }); + + it("returns null when the key does not exist", async () => { + const result = await service.get("key"); + expect(result).toBeNull(); + }); + + it("returns null when the stored object is null", async () => { + store[key] = null; + + const result = await service.get(key); + expect(result).toBeNull(); + }); + }); +}); diff --git a/apps/browser/src/platform/services/abstractions/script-injector.service.ts b/apps/browser/src/platform/services/abstractions/script-injector.service.ts new file mode 100644 index 0000000000..b41e5c7617 --- /dev/null +++ b/apps/browser/src/platform/services/abstractions/script-injector.service.ts @@ -0,0 +1,45 @@ +export type CommonScriptInjectionDetails = { + /** + * Script injected into the document. + * Overridden by `mv2Details` and `mv3Details`. + */ + file?: string; + /** + * Identifies the frame targeted for script injection. Defaults to the top level frame (0). + * Can also be set to "all_frames" to inject into all frames in a tab. + */ + frame?: "all_frames" | number; + /** + * When the script executes. Defaults to "document_start". + * @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/content_scripts + */ + runAt?: "document_start" | "document_end" | "document_idle"; +}; + +export type Mv2ScriptInjectionDetails = { + file: string; +}; + +export type Mv3ScriptInjectionDetails = { + file: string; + /** + * The world in which the script should be executed. Defaults to "ISOLATED". + * @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld + */ + world?: chrome.scripting.ExecutionWorld; +}; + +/** + * Configuration for injecting a script into a tab. The `file` property should present as a + * path that is relative to the root directory of the extension build, ie "content/script.js". + */ +export type ScriptInjectionConfig = { + tabId: number; + injectDetails: CommonScriptInjectionDetails; + mv2Details?: Mv2ScriptInjectionDetails; + mv3Details?: Mv3ScriptInjectionDetails; +}; + +export abstract class ScriptInjectorService { + abstract inject(config: ScriptInjectionConfig): Promise; +} diff --git a/apps/browser/src/platform/services/browser-crypto.service.ts b/apps/browser/src/platform/services/browser-crypto.service.ts index 969dbdf761..d7533a22d6 100644 --- a/apps/browser/src/platform/services/browser-crypto.service.ts +++ b/apps/browser/src/platform/services/browser-crypto.service.ts @@ -1,6 +1,7 @@ import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; @@ -17,6 +18,7 @@ import { UserKey } from "@bitwarden/common/types/key"; export class BrowserCryptoService extends CryptoService { constructor( + masterPasswordService: InternalMasterPasswordServiceAbstraction, keyGenerationService: KeyGenerationService, cryptoFunctionService: CryptoFunctionService, encryptService: EncryptService, @@ -28,6 +30,7 @@ export class BrowserCryptoService extends CryptoService { private biometricStateService: BiometricStateService, ) { super( + masterPasswordService, keyGenerationService, cryptoFunctionService, encryptService, diff --git a/apps/browser/src/platform/services/browser-local-storage.service.spec.ts b/apps/browser/src/platform/services/browser-local-storage.service.spec.ts new file mode 100644 index 0000000000..37ea37dbf6 --- /dev/null +++ b/apps/browser/src/platform/services/browser-local-storage.service.spec.ts @@ -0,0 +1,89 @@ +import { objToStore } from "./abstractions/abstract-chrome-storage-api.service"; +import BrowserLocalStorageService from "./browser-local-storage.service"; + +describe("BrowserLocalStorageService", () => { + let service: BrowserLocalStorageService; + let store: Record; + + beforeEach(() => { + store = {}; + + service = new BrowserLocalStorageService(); + }); + + describe("clear", () => { + let clearMock: jest.Mock; + + beforeEach(() => { + clearMock = chrome.storage.local.clear as jest.Mock; + }); + + it("uses the api to clear", async () => { + await service.clear(); + + expect(clearMock).toHaveBeenCalledTimes(1); + }); + }); + + describe("getAll", () => { + let getMock: jest.Mock; + + beforeEach(() => { + // setup get + getMock = chrome.storage.local.get as jest.Mock; + getMock.mockImplementation((key, callback) => { + if (key == null) { + callback(store); + } else { + callback({ [key]: store[key] }); + } + }); + }); + + it("returns all values", async () => { + store["key1"] = "string"; + store["key2"] = 0; + const result = await service.getAll(); + + expect(result).toEqual(store); + }); + + it("handles empty stores", async () => { + const result = await service.getAll(); + + expect(result).toEqual({}); + }); + + it("handles stores with null values", async () => { + store["key"] = null; + + const result = await service.getAll(); + expect(result).toEqual(store); + }); + + it("handles values processed for storage", async () => { + const obj = { test: 2 }; + const key = "key"; + store[key] = objToStore(obj); + + const result = await service.getAll(); + + expect(result).toEqual({ + [key]: obj, + }); + }); + + // This is a test of backwards compatibility before local storage was serialized. + it("handles values that were stored without processing for storage", async () => { + const obj = { test: 2 }; + const key = "key"; + store[key] = obj; + + const result = await service.getAll(); + + expect(result).toEqual({ + [key]: obj, + }); + }); + }); +}); diff --git a/apps/browser/src/platform/services/browser-local-storage.service.ts b/apps/browser/src/platform/services/browser-local-storage.service.ts index 2efd03a046..e1f9f63676 100644 --- a/apps/browser/src/platform/services/browser-local-storage.service.ts +++ b/apps/browser/src/platform/services/browser-local-storage.service.ts @@ -4,4 +4,32 @@ export default class BrowserLocalStorageService extends AbstractChromeStorageSer constructor() { super(chrome.storage.local); } + + /** + * Clears local storage + */ + async clear() { + await chrome.storage.local.clear(); + } + + /** + * Retrieves all objects stored in local storage. + * + * @remarks This method processes values prior to resolving, do not use `chrome.storage.local` directly + * @returns Promise resolving to keyed object of all stored data + */ + async getAll(): Promise> { + return new Promise((resolve) => { + this.chromeStorageApi.get(null, (allStorage) => { + const resolved = Object.entries(allStorage).reduce( + (agg, [key, value]) => { + agg[key] = this.processGetObject(value); + return agg; + }, + {} as Record, + ); + resolve(resolved); + }); + }); + } } diff --git a/apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts b/apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts deleted file mode 100644 index 0c7008473b..0000000000 --- a/apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -export default class BrowserMessagingPrivateModeBackgroundService implements MessagingService { - send(subscriber: string, arg: any = {}) { - const message = Object.assign({}, { command: subscriber }, arg); - (self as any).bitwardenPopupMainMessageListener(message); - } -} diff --git a/apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts b/apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts deleted file mode 100644 index 5883f61197..0000000000 --- a/apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -export default class BrowserMessagingPrivateModePopupService implements MessagingService { - send(subscriber: string, arg: any = {}) { - const message = Object.assign({}, { command: subscriber }, arg); - (self as any).bitwardenBackgroundMessageListener(message); - } -} diff --git a/apps/browser/src/platform/services/browser-messaging.service.ts b/apps/browser/src/platform/services/browser-messaging.service.ts deleted file mode 100644 index 5eff957cb5..0000000000 --- a/apps/browser/src/platform/services/browser-messaging.service.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -import { BrowserApi } from "../browser/browser-api"; - -export default class BrowserMessagingService implements MessagingService { - send(subscriber: string, arg: any = {}) { - return BrowserApi.sendMessage(subscriber, arg); - } -} diff --git a/apps/browser/src/platform/services/browser-script-injector.service.spec.ts b/apps/browser/src/platform/services/browser-script-injector.service.spec.ts new file mode 100644 index 0000000000..6ae84c6464 --- /dev/null +++ b/apps/browser/src/platform/services/browser-script-injector.service.spec.ts @@ -0,0 +1,173 @@ +import { BrowserApi } from "../browser/browser-api"; + +import { + CommonScriptInjectionDetails, + Mv3ScriptInjectionDetails, +} from "./abstractions/script-injector.service"; +import { BrowserScriptInjectorService } from "./browser-script-injector.service"; + +describe("ScriptInjectorService", () => { + const tabId = 1; + const combinedManifestVersionFile = "content/autofill-init.js"; + const mv2SpecificFile = "content/autofill-init-mv2.js"; + const mv2Details = { file: mv2SpecificFile }; + const mv3SpecificFile = "content/autofill-init-mv3.js"; + const mv3Details: Mv3ScriptInjectionDetails = { file: mv3SpecificFile, world: "MAIN" }; + const sharedInjectDetails: CommonScriptInjectionDetails = { + runAt: "document_start", + }; + const manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get"); + let scriptInjectorService: BrowserScriptInjectorService; + jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation(); + jest.spyOn(BrowserApi, "isManifestVersion"); + + beforeEach(() => { + scriptInjectorService = new BrowserScriptInjectorService(); + }); + + describe("inject", () => { + describe("injection of a single script that functions in both manifest v2 and v3", () => { + it("injects the script in manifest v2 when given combined injection details", async () => { + manifestVersionSpy.mockReturnValue(2); + + await scriptInjectorService.inject({ + tabId, + injectDetails: { + file: combinedManifestVersionFile, + frame: "all_frames", + ...sharedInjectDetails, + }, + }); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabId, { + ...sharedInjectDetails, + allFrames: true, + file: combinedManifestVersionFile, + }); + }); + + it("injects the script in manifest v3 when given combined injection details", async () => { + manifestVersionSpy.mockReturnValue(3); + + await scriptInjectorService.inject({ + tabId, + injectDetails: { + file: combinedManifestVersionFile, + frame: 10, + ...sharedInjectDetails, + }, + }); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith( + tabId, + { ...sharedInjectDetails, frameId: 10, file: combinedManifestVersionFile }, + { world: "ISOLATED" }, + ); + }); + }); + + describe("injection of mv2 specific details", () => { + describe("given the extension is running manifest v2", () => { + it("injects the mv2 script injection details file", async () => { + manifestVersionSpy.mockReturnValue(2); + + await scriptInjectorService.inject({ + mv2Details, + tabId, + injectDetails: sharedInjectDetails, + }); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabId, { + ...sharedInjectDetails, + frameId: 0, + file: mv2SpecificFile, + }); + }); + }); + + describe("given the extension is running manifest v3", () => { + it("injects the common script injection details file", async () => { + manifestVersionSpy.mockReturnValue(3); + + await scriptInjectorService.inject({ + mv2Details, + tabId, + injectDetails: { ...sharedInjectDetails, file: combinedManifestVersionFile }, + }); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith( + tabId, + { + ...sharedInjectDetails, + frameId: 0, + file: combinedManifestVersionFile, + }, + { world: "ISOLATED" }, + ); + }); + + it("throws an error if no common script injection details file is specified", async () => { + manifestVersionSpy.mockReturnValue(3); + + await expect( + scriptInjectorService.inject({ + mv2Details, + tabId, + injectDetails: { ...sharedInjectDetails, file: null }, + }), + ).rejects.toThrow("No file specified for script injection"); + }); + }); + }); + + describe("injection of mv3 specific details", () => { + describe("given the extension is running manifest v3", () => { + it("injects the mv3 script injection details file", async () => { + manifestVersionSpy.mockReturnValue(3); + + await scriptInjectorService.inject({ + mv3Details, + tabId, + injectDetails: sharedInjectDetails, + }); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith( + tabId, + { ...sharedInjectDetails, frameId: 0, file: mv3SpecificFile }, + { world: "MAIN" }, + ); + }); + }); + + describe("given the extension is running manifest v2", () => { + it("injects the common script injection details file", async () => { + manifestVersionSpy.mockReturnValue(2); + + await scriptInjectorService.inject({ + mv3Details, + tabId, + injectDetails: { ...sharedInjectDetails, file: combinedManifestVersionFile }, + }); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabId, { + ...sharedInjectDetails, + frameId: 0, + file: combinedManifestVersionFile, + }); + }); + + it("throws an error if no common script injection details file is specified", async () => { + manifestVersionSpy.mockReturnValue(2); + + await expect( + scriptInjectorService.inject({ + mv3Details, + tabId, + injectDetails: { ...sharedInjectDetails, file: "" }, + }), + ).rejects.toThrow("No file specified for script injection"); + }); + }); + }); + }); +}); diff --git a/apps/browser/src/platform/services/browser-script-injector.service.ts b/apps/browser/src/platform/services/browser-script-injector.service.ts new file mode 100644 index 0000000000..54513188d5 --- /dev/null +++ b/apps/browser/src/platform/services/browser-script-injector.service.ts @@ -0,0 +1,78 @@ +import { BrowserApi } from "../browser/browser-api"; + +import { + CommonScriptInjectionDetails, + ScriptInjectionConfig, + ScriptInjectorService, +} from "./abstractions/script-injector.service"; + +export class BrowserScriptInjectorService extends ScriptInjectorService { + /** + * Facilitates the injection of a script into a tab context. Will adjust + * behavior between manifest v2 and v3 based on the passed configuration. + * + * @param config - The configuration for the script injection. + */ + async inject(config: ScriptInjectionConfig): Promise { + const { tabId, injectDetails, mv3Details } = config; + const file = this.getScriptFile(config); + if (!file) { + throw new Error("No file specified for script injection"); + } + + const injectionDetails = this.buildInjectionDetails(injectDetails, file); + + if (BrowserApi.isManifestVersion(3)) { + await BrowserApi.executeScriptInTab(tabId, injectionDetails, { + world: mv3Details?.world ?? "ISOLATED", + }); + + return; + } + + await BrowserApi.executeScriptInTab(tabId, injectionDetails); + } + + /** + * Retrieves the script file to inject based on the configuration. + * + * @param config - The configuration for the script injection. + */ + private getScriptFile(config: ScriptInjectionConfig): string { + const { injectDetails, mv2Details, mv3Details } = config; + + if (BrowserApi.isManifestVersion(3)) { + return mv3Details?.file ?? injectDetails?.file; + } + + return mv2Details?.file ?? injectDetails?.file; + } + + /** + * Builds the injection details for the script injection. + * + * @param injectDetails - The details for the script injection. + * @param file - The file to inject. + */ + private buildInjectionDetails( + injectDetails: CommonScriptInjectionDetails, + file: string, + ): chrome.tabs.InjectDetails { + const { frame, runAt } = injectDetails; + const injectionDetails: chrome.tabs.InjectDetails = { file }; + + if (runAt) { + injectionDetails.runAt = runAt; + } + + if (!frame) { + return { ...injectionDetails, frameId: 0 }; + } + + if (frame !== "all_frames") { + return { ...injectionDetails, frameId: frame }; + } + + return { ...injectionDetails, allFrames: true }; + } +} diff --git a/apps/browser/src/platform/services/browser-state.service.spec.ts b/apps/browser/src/platform/services/browser-state.service.spec.ts index 7e75b9b707..8f43998321 100644 --- a/apps/browser/src/platform/services/browser-state.service.spec.ts +++ b/apps/browser/src/platform/services/browser-state.service.spec.ts @@ -1,4 +1,5 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { firstValueFrom } from "rxjs"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -12,15 +13,11 @@ import { GlobalState } from "@bitwarden/common/platform/models/domain/global-sta import { State } from "@bitwarden/common/platform/models/domain/state"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { mockAccountServiceWith } from "@bitwarden/common/spec"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; -import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { UserId } from "@bitwarden/common/types/guid"; import { Account } from "../../models/account"; -import { BrowserComponentState } from "../../models/browserComponentState"; -import { BrowserSendComponentState } from "../../models/browserSendComponentState"; -import { BrowserStateService } from "./browser-state.service"; +import { DefaultBrowserStateService } from "./default-browser-state.service"; // disable session syncing to just test class jest.mock("../decorators/session-sync-observable/"); @@ -39,7 +36,7 @@ describe("Browser State Service", () => { const userId = "userId" as UserId; const accountService = mockAccountServiceWith(userId); - let sut: BrowserStateService; + let sut: DefaultBrowserStateService; beforeEach(() => { secureStorageService = mock(); @@ -71,7 +68,7 @@ describe("Browser State Service", () => { const stateGetter = (key: string) => Promise.resolve(state); memoryStorageService.get.mockImplementation(stateGetter); - sut = new BrowserStateService( + sut = new DefaultBrowserStateService( diskStorageService, secureStorageService, memoryStorageService, @@ -85,32 +82,17 @@ describe("Browser State Service", () => { ); }); - describe("getBrowserSendComponentState", () => { - it("should return a BrowserSendComponentState", async () => { - const sendState = new BrowserSendComponentState(); - sendState.sends = [new SendView(), new SendView()]; - sendState.typeCounts = new Map([ - [SendType.File, 3], - [SendType.Text, 5], - ]); - state.accounts[userId].send = sendState; - (global as any)["watch"] = state; + describe("add Account", () => { + it("should add account", async () => { + const newUserId = "newUserId" as UserId; + const newAcct = new Account({ + profile: { userId: newUserId }, + }); - const actual = await sut.getBrowserSendComponentState(); - expect(actual).toBeInstanceOf(BrowserSendComponentState); - expect(actual).toMatchObject(sendState); - }); - }); + await sut.addAccount(newAcct); - describe("getBrowserSendTypeComponentState", () => { - it("should return a BrowserComponentState", async () => { - const componentState = new BrowserComponentState(); - componentState.scrollY = 0; - componentState.searchText = "test"; - state.accounts[userId].sendType = componentState; - - const actual = await sut.getBrowserSendTypeComponentState(); - expect(actual).toStrictEqual(componentState); + const accts = await firstValueFrom(sut.accounts$); + expect(accts[newUserId]).toBeDefined(); }); }); }); diff --git a/apps/browser/src/platform/services/browser-state.service.ts b/apps/browser/src/platform/services/default-browser-state.service.ts similarity index 74% rename from apps/browser/src/platform/services/browser-state.service.ts rename to apps/browser/src/platform/services/default-browser-state.service.ts index ea410ee83a..f1f306dbc0 100644 --- a/apps/browser/src/platform/services/browser-state.service.ts +++ b/apps/browser/src/platform/services/default-browser-state.service.ts @@ -15,17 +15,15 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service"; import { Account } from "../../models/account"; -import { BrowserComponentState } from "../../models/browserComponentState"; -import { BrowserSendComponentState } from "../../models/browserSendComponentState"; import { BrowserApi } from "../browser/browser-api"; import { browserSession, sessionSync } from "../decorators/session-sync-observable"; -import { BrowserStateService as StateServiceAbstraction } from "./abstractions/browser-state.service"; +import { BrowserStateService } from "./abstractions/browser-state.service"; @browserSession -export class BrowserStateService +export class DefaultBrowserStateService extends BaseStateService - implements StateServiceAbstraction + implements BrowserStateService { @sessionSync({ initializer: Account.fromJSON as any, // TODO: Remove this any when all any types are removed from Account @@ -115,46 +113,6 @@ export class BrowserStateService ); } - async getBrowserSendComponentState(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.send; - } - - async setBrowserSendComponentState( - value: BrowserSendComponentState, - options?: StorageOptions, - ): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.send = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - - async getBrowserSendTypeComponentState(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.sendType; - } - - async setBrowserSendTypeComponentState( - value: BrowserComponentState, - options?: StorageOptions, - ): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.sendType = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - // Overriding the base class to prevent deleting the cache on save. We register a storage listener // to delete the cache in the constructor above. protected override async saveAccountToDisk( diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts index 7740a22071..a4581e6ac1 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts @@ -2,6 +2,8 @@ import { mock, MockProxy } from "jest-mock-extended"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { AbstractMemoryStorageService, AbstractStorageService, @@ -11,16 +13,26 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { BrowserApi } from "../browser/browser-api"; + import { LocalBackedSessionStorageService } from "./local-backed-session-storage.service"; -describe("LocalBackedSessionStorage", () => { +describe.skip("LocalBackedSessionStorage", () => { + const sendMessageWithResponseSpy: jest.SpyInstance = jest.spyOn( + BrowserApi, + "sendMessageWithResponse", + ); + let encryptService: MockProxy; let keyGenerationService: MockProxy; let localStorageService: MockProxy; let sessionStorageService: MockProxy; + let logService: MockProxy; + let platformUtilsService: MockProxy; - let cache: Map; + let cache: Record; const testObj = { a: 1, b: 2 }; + const stringifiedTestObj = JSON.stringify(testObj); const key = new SymmetricCryptoKey(Utils.fromUtf8ToArray("00000000000000000000000000000000")); let getSessionKeySpy: jest.SpyInstance; @@ -40,20 +52,24 @@ describe("LocalBackedSessionStorage", () => { }; beforeEach(() => { + sendMessageWithResponseSpy.mockResolvedValue(null); + logService = mock(); encryptService = mock(); keyGenerationService = mock(); localStorageService = mock(); sessionStorageService = mock(); sut = new LocalBackedSessionStorageService( + logService, encryptService, keyGenerationService, localStorageService, sessionStorageService, + platformUtilsService, "test", ); - cache = sut["cache"]; + cache = sut["cachedSession"]; keyGenerationService.createKeyWithPurpose.mockResolvedValue({ derivedKey: key, @@ -64,19 +80,27 @@ describe("LocalBackedSessionStorage", () => { getSessionKeySpy = jest.spyOn(sut, "getSessionEncKey"); getSessionKeySpy.mockResolvedValue(key); - sendUpdateSpy = jest.spyOn(sut, "sendUpdate"); - sendUpdateSpy.mockReturnValue(); + // sendUpdateSpy = jest.spyOn(sut, "sendUpdate"); + // sendUpdateSpy.mockReturnValue(); }); describe("get", () => { - it("should return from cache", async () => { - cache.set("test", testObj); - const result = await sut.get("test"); - expect(result).toStrictEqual(testObj); + describe("in local cache or external context cache", () => { + it("should return from local cache", async () => { + cache["test"] = stringifiedTestObj; + const result = await sut.get("test"); + expect(result).toStrictEqual(testObj); + }); + + it("should return from external context cache when local cache is not available", async () => { + sendMessageWithResponseSpy.mockResolvedValue(stringifiedTestObj); + const result = await sut.get("test"); + expect(result).toStrictEqual(testObj); + }); }); describe("not in cache", () => { - const session = { test: testObj }; + const session = { test: stringifiedTestObj }; beforeEach(() => { mockExistingSessionKey(key); @@ -117,8 +141,8 @@ describe("LocalBackedSessionStorage", () => { it("should set retrieved values in cache", async () => { await sut.get("test"); - expect(cache.has("test")).toBe(true); - expect(cache.get("test")).toEqual(session.test); + expect(cache["test"]).toBeTruthy(); + expect(cache["test"]).toEqual(session.test); }); it("should use a deserializer if provided", async () => { @@ -148,13 +172,56 @@ describe("LocalBackedSessionStorage", () => { }); describe("remove", () => { + describe("existing cache value is null", () => { + it("should not save null if the local cached value is already null", async () => { + cache["test"] = null; + await sut.remove("test"); + expect(sendUpdateSpy).not.toHaveBeenCalled(); + }); + + it("should not save null if the externally cached value is already null", async () => { + sendMessageWithResponseSpy.mockResolvedValue(null); + await sut.remove("test"); + expect(sendUpdateSpy).not.toHaveBeenCalled(); + }); + }); + it("should save null", async () => { + cache["test"] = stringifiedTestObj; + await sut.remove("test"); expect(sendUpdateSpy).toHaveBeenCalledWith({ key: "test", updateType: "remove" }); }); }); describe("save", () => { + describe("currently cached", () => { + it("does not save the value a local cached value exists which is an exact match", async () => { + cache["test"] = stringifiedTestObj; + await sut.save("test", testObj); + expect(sendUpdateSpy).not.toHaveBeenCalled(); + }); + + it("does not save the value if a local cached value exists, even if the keys not in the same order", async () => { + cache["test"] = JSON.stringify({ b: 2, a: 1 }); + await sut.save("test", testObj); + expect(sendUpdateSpy).not.toHaveBeenCalled(); + }); + + it("does not save the value a externally cached value exists which is an exact match", async () => { + sendMessageWithResponseSpy.mockResolvedValue(stringifiedTestObj); + await sut.save("test", testObj); + expect(sendUpdateSpy).not.toHaveBeenCalled(); + expect(cache["test"]).toBe(stringifiedTestObj); + }); + + it("saves the value if the currently cached string value evaluates to a falsy value", async () => { + cache["test"] = "null"; + await sut.save("test", testObj); + expect(sendUpdateSpy).toHaveBeenCalledWith({ key: "test", updateType: "save" }); + }); + }); + describe("caching", () => { beforeEach(() => { localStorageService.get.mockResolvedValue(null); @@ -167,21 +234,21 @@ describe("LocalBackedSessionStorage", () => { }); it("should remove key from cache if value is null", async () => { - cache.set("test", {}); - const cacheSetSpy = jest.spyOn(cache, "set"); - expect(cache.has("test")).toBe(true); + cache["test"] = {}; + // const cacheSetSpy = jest.spyOn(cache, "set"); + expect(cache["test"]).toBe(true); await sut.save("test", null); // Don't remove from cache, just replace with null - expect(cache.get("test")).toBe(null); - expect(cacheSetSpy).toHaveBeenCalledWith("test", null); + expect(cache["test"]).toBe(null); + // expect(cacheSetSpy).toHaveBeenCalledWith("test", null); }); it("should set cache if value is non-null", async () => { - expect(cache.has("test")).toBe(false); - const setSpy = jest.spyOn(cache, "set"); + expect(cache["test"]).toBe(false); + // const setSpy = jest.spyOn(cache, "set"); await sut.save("test", testObj); - expect(cache.get("test")).toBe(testObj); - expect(setSpy).toHaveBeenCalledWith("test", testObj); + expect(cache["test"]).toBe(stringifiedTestObj); + // expect(setSpy).toHaveBeenCalledWith("test", stringifiedTestObj); }); }); diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts index 3f01e4169e..146eb11b2b 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts @@ -1,8 +1,10 @@ -import { Observable, Subject, filter, map, merge, share, tap } from "rxjs"; +import { Subject } from "rxjs"; import { Jsonify } from "type-fest"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { AbstractMemoryStorageService, AbstractStorageService, @@ -13,57 +15,77 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { MemoryStorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { fromChromeEvent } from "../browser/from-chrome-event"; +import { BrowserApi } from "../browser/browser-api"; import { devFlag } from "../decorators/dev-flag.decorator"; import { devFlagEnabled } from "../flags"; +import { MemoryStoragePortMessage } from "../storage/port-messages"; +import { portName } from "../storage/port-name"; export class LocalBackedSessionStorageService extends AbstractMemoryStorageService implements ObservableStorageService { - private cache = new Map(); private updatesSubject = new Subject(); - - private commandName = `localBackedSessionStorage_${this.name}`; - private encKey = `localEncryptionKey_${this.name}`; - private sessionKey = `session_${this.name}`; - - updates$: Observable; + private commandName = `localBackedSessionStorage_${this.partitionName}`; + private encKey = `localEncryptionKey_${this.partitionName}`; + private sessionKey = `session_${this.partitionName}`; + private cachedSession: Record = {}; + private _ports: Set = new Set([]); + private knownNullishCacheKeys: Set = new Set([]); constructor( + private logService: LogService, private encryptService: EncryptService, private keyGenerationService: KeyGenerationService, private localStorage: AbstractStorageService, private sessionStorage: AbstractStorageService, - private name: string, + private platformUtilsService: PlatformUtilsService, + private partitionName: string, ) { super(); - const remoteObservable = fromChromeEvent(chrome.runtime.onMessage).pipe( - filter(([msg]) => msg.command === this.commandName), - map(([msg]) => msg.update as StorageUpdate), - tap((update) => { - if (update.updateType === "remove") { - this.cache.set(update.key, null); - } else { - this.cache.delete(update.key); - } - }), - share(), - ); + BrowserApi.addListener(chrome.runtime.onConnect, (port) => { + if (port.name !== `${portName(chrome.storage.session)}_${partitionName}`) { + return; + } - remoteObservable.subscribe(); + this._ports.add(port); - this.updates$ = merge(this.updatesSubject.asObservable(), remoteObservable); + const listenerCallback = this.onMessageFromForeground.bind(this); + port.onDisconnect.addListener(() => { + this._ports.delete(port); + port.onMessage.removeListener(listenerCallback); + }); + port.onMessage.addListener(listenerCallback); + // Initialize the new memory storage service with existing data + this.sendMessageTo(port, { + action: "initialization", + data: Array.from(Object.keys(this.cachedSession)), + }); + }); + this.updates$.subscribe((update) => { + this.broadcastMessage({ + action: "subject_update", + data: update, + }); + }); } get valuesRequireDeserialization(): boolean { return true; } + get updates$() { + return this.updatesSubject.asObservable(); + } + async get(key: string, options?: MemoryStorageOptions): Promise { - if (this.cache.has(key)) { - return this.cache.get(key) as T; + if (this.cachedSession[key] != null) { + return this.cachedSession[key] as T; + } + + if (this.knownNullishCacheKeys.has(key)) { + return null; } return await this.getBypassCache(key, options); @@ -71,7 +93,8 @@ export class LocalBackedSessionStorageService async getBypassCache(key: string, options?: MemoryStorageOptions): Promise { const session = await this.getLocalSession(await this.getSessionEncKey()); - if (session == null || !Object.keys(session).includes(key)) { + if (session[key] == null) { + this.knownNullishCacheKeys.add(key); return null; } @@ -80,8 +103,8 @@ export class LocalBackedSessionStorageService value = options.deserializer(value as Jsonify); } - this.cache.set(key, value); - return this.cache.get(key) as T; + void this.save(key, value); + return value as T; } async has(key: string): Promise { @@ -89,41 +112,48 @@ export class LocalBackedSessionStorageService } async save(key: string, obj: T): Promise { + // This is for observation purposes only. At some point, we don't want to write to local session storage if the value is the same. + if (this.platformUtilsService.isDev()) { + const existingValue = this.cachedSession[key] as T; + if (this.compareValues(existingValue, obj)) { + this.logService.warning(`Possible unnecessary write to local session storage. Key: ${key}`); + this.logService.warning(obj as any); + } + } + if (obj == null) { return await this.remove(key); } - this.cache.set(key, obj); + this.knownNullishCacheKeys.delete(key); + this.cachedSession[key] = obj; await this.updateLocalSessionValue(key, obj); - this.sendUpdate({ key, updateType: "save" }); + this.updatesSubject.next({ key, updateType: "save" }); } async remove(key: string): Promise { - this.cache.set(key, null); + this.knownNullishCacheKeys.add(key); + delete this.cachedSession[key]; await this.updateLocalSessionValue(key, null); - this.sendUpdate({ key, updateType: "remove" }); - } - - sendUpdate(storageUpdate: StorageUpdate) { - this.updatesSubject.next(storageUpdate); - void chrome.runtime.sendMessage({ - command: this.commandName, - update: storageUpdate, - }); + this.updatesSubject.next({ key, updateType: "remove" }); } private async updateLocalSessionValue(key: string, obj: T) { const sessionEncKey = await this.getSessionEncKey(); const localSession = (await this.getLocalSession(sessionEncKey)) ?? {}; localSession[key] = obj; - await this.setLocalSession(localSession, sessionEncKey); + void this.setLocalSession(localSession, sessionEncKey); } async getLocalSession(encKey: SymmetricCryptoKey): Promise> { - const local = await this.localStorage.get(this.sessionKey); + if (Object.keys(this.cachedSession).length > 0) { + return this.cachedSession; + } + this.cachedSession = {}; + const local = await this.localStorage.get(this.sessionKey); if (local == null) { - return null; + return this.cachedSession; } if (devFlagEnabled("storeSessionDecrypted")) { @@ -135,9 +165,11 @@ export class LocalBackedSessionStorageService // Error with decryption -- session is lost, delete state and key and start over await this.setSessionEncKey(null); await this.localStorage.remove(this.sessionKey); - return null; + return this.cachedSession; } - return JSON.parse(sessionJson); + + this.cachedSession = JSON.parse(sessionJson); + return this.cachedSession; } async setLocalSession(session: Record, key: SymmetricCryptoKey) { @@ -192,4 +224,76 @@ export class LocalBackedSessionStorageService await this.sessionStorage.save(this.encKey, input); } } + + private compareValues(value1: T, value2: T): boolean { + if (value1 == null && value2 == null) { + return true; + } + + if (value1 && value2 == null) { + return false; + } + + if (value1 == null && value2) { + return false; + } + + if (typeof value1 !== "object" || typeof value2 !== "object") { + return value1 === value2; + } + + if (JSON.stringify(value1) === JSON.stringify(value2)) { + return true; + } + + return Object.entries(value1).sort().toString() === Object.entries(value2).sort().toString(); + } + + private async onMessageFromForeground( + message: MemoryStoragePortMessage, + port: chrome.runtime.Port, + ) { + if (message.originator === "background") { + return; + } + + let result: unknown = null; + + switch (message.action) { + case "get": + case "getBypassCache": + case "has": { + result = await this[message.action](message.key); + break; + } + case "save": + await this.save(message.key, JSON.parse((message.data as string) ?? null) as unknown); + break; + case "remove": + await this.remove(message.key); + break; + } + + this.sendMessageTo(port, { + id: message.id, + key: message.key, + data: JSON.stringify(result), + }); + } + + protected broadcastMessage(data: Omit) { + this._ports.forEach((port) => { + this.sendMessageTo(port, data); + }); + } + + private sendMessageTo( + port: chrome.runtime.Port, + data: Omit, + ) { + port.postMessage({ + ...data, + originator: "background", + }); + } } diff --git a/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts index 8cf1a8d3e4..24aa45d5c3 100644 --- a/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts @@ -1,13 +1,10 @@ -import { SecurityContext } from "@angular/core"; -import { DomSanitizer } from "@angular/platform-browser"; -import { ToastrService } from "ngx-toastr"; +import { ToastService } from "@bitwarden/components"; import { BrowserPlatformUtilsService } from "./browser-platform-utils.service"; export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService { constructor( - private sanitizer: DomSanitizer, - private toastrService: ToastrService, + private toastService: ToastService, clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, biometricCallback: () => Promise, win: Window & typeof globalThis, @@ -21,20 +18,6 @@ export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService text: string | string[], options?: any, ): void { - if (typeof text === "string") { - // Already in the correct format - } else if (text.length === 1) { - text = text[0]; - } else { - let message = ""; - text.forEach( - (t: string) => - (message += "

" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "

"), - ); - text = message; - options.enableHtml = true; - } - this.toastrService.show(text, title, options, "toast-" + type); - // noop + this.toastService._showToast({ type, title, text, options }); } } diff --git a/apps/browser/src/platform/storage/foreground-memory-storage.service.ts b/apps/browser/src/platform/storage/foreground-memory-storage.service.ts index 1e5220002a..b3ac8de55e 100644 --- a/apps/browser/src/platform/storage/foreground-memory-storage.service.ts +++ b/apps/browser/src/platform/storage/foreground-memory-storage.service.ts @@ -21,12 +21,16 @@ export class ForegroundMemoryStorageService extends AbstractMemoryStorageService } updates$; - constructor() { + constructor(private partitionName?: string) { super(); this.updates$ = this.updatesSubject.asObservable(); - this._port = chrome.runtime.connect({ name: portName(chrome.storage.session) }); + let name = portName(chrome.storage.session); + if (this.partitionName) { + name = `${name}_${this.partitionName}`; + } + this._port = chrome.runtime.connect({ name }); this._backgroundResponses$ = fromChromeEvent(this._port.onMessage).pipe( map(([message]) => message), filter((message) => message.originator === "background"), diff --git a/apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts b/apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts new file mode 100644 index 0000000000..e30f35b680 --- /dev/null +++ b/apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts @@ -0,0 +1,26 @@ +import { map, share } from "rxjs"; + +import { tagAsExternal } from "@bitwarden/common/platform/messaging/internal"; + +import { fromChromeEvent } from "../browser/from-chrome-event"; + +/** + * Creates an observable that listens to messages through `chrome.runtime.onMessage`. + * @returns An observable stream of messages. + */ +export const fromChromeRuntimeMessaging = () => { + return fromChromeEvent(chrome.runtime.onMessage).pipe( + map(([message, sender]) => { + message ??= {}; + + // Force the sender onto the message as long as we won't overwrite anything + if (!("webExtSender" in message)) { + message.webExtSender = sender; + } + + return message; + }), + tagAsExternal, + share(), + ); +}; diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index e0d898481b..7acaf1ba93 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -1,17 +1,18 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; -import { ToastrService } from "ngx-toastr"; -import { filter, concatMap, Subject, takeUntil, firstValueFrom } from "rxjs"; +import { filter, concatMap, Subject, takeUntil, firstValueFrom, tap, map } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { DialogService, SimpleDialogOptions } from "@bitwarden/components"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { MessageListener } from "@bitwarden/common/platform/messaging"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components"; import { BrowserApi } from "../platform/browser/browser-api"; -import { ZonedMessageListenerService } from "../platform/browser/zoned-message-listener.service"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; -import { ForegroundPlatformUtilsService } from "../platform/services/platform-utils/foreground-platform-utils.service"; +import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service"; import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service"; import { routerTransition } from "./app-routing.animations"; @@ -32,18 +33,19 @@ export class AppComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); constructor( - private toastrService: ToastrService, - private broadcasterService: BroadcasterService, private authService: AuthService, private i18nService: I18nService, private router: Router, private stateService: BrowserStateService, + private browserSendStateService: BrowserSendStateService, private vaultBrowserStateService: VaultBrowserStateService, + private cipherService: CipherService, private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone, - private platformUtilsService: ForegroundPlatformUtilsService, + private platformUtilsService: PlatformUtilsService, private dialogService: DialogService, - private browserMessagingApi: ZonedMessageListenerService, + private messageListener: MessageListener, + private toastService: ToastService, ) {} async ngOnInit() { @@ -55,8 +57,9 @@ export class AppComponent implements OnInit, OnDestroy { this.activeUserId = userId; }); - this.stateService.activeAccountUnlocked$ + this.authService.activeAccountStatus$ .pipe( + map((status) => status === AuthenticationStatus.Unlocked), filter((unlocked) => unlocked), concatMap(async () => { await this.recordActivity(); @@ -73,77 +76,76 @@ export class AppComponent implements OnInit, OnDestroy { window.onkeypress = () => this.recordActivity(); }); - const bitwardenPopupMainMessageListener = (msg: any, sender: any) => { - if (msg.command === "doneLoggingOut") { - this.authService.logOut(async () => { - if (msg.expired) { - this.showToast({ - type: "warning", - title: this.i18nService.t("loggedOut"), - text: this.i18nService.t("loginExpired"), + this.messageListener.allMessages$ + .pipe( + tap((msg: any) => { + if (msg.command === "doneLoggingOut") { + this.authService.logOut(async () => { + if (msg.expired) { + this.toastService.showToast({ + variant: "warning", + title: this.i18nService.t("loggedOut"), + message: this.i18nService.t("loginExpired"), + }); + } + + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router.navigate(["home"]); }); + this.changeDetectorRef.detectChanges(); + } else if (msg.command === "authBlocked") { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router.navigate(["home"]); + } else if ( + msg.command === "locked" && + (msg.userId == null || msg.userId == this.activeUserId) + ) { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router.navigate(["lock"]); + } else if (msg.command === "showDialog") { + // 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.showDialog(msg); + } else if (msg.command === "showNativeMessagingFinterprintDialog") { + // TODO: Should be refactored to live in another service. + // 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.showNativeMessagingFingerprintDialog(msg); + } else if (msg.command === "showToast") { + this.toastService._showToast(msg); + } else if (msg.command === "reloadProcess") { + const forceWindowReload = + this.platformUtilsService.isSafari() || + this.platformUtilsService.isFirefox() || + this.platformUtilsService.isOpera(); + // Wait to make sure background has reloaded first. + window.setTimeout( + () => BrowserApi.reloadExtension(forceWindowReload ? window : null), + 2000, + ); + } else if (msg.command === "reloadPopup") { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router.navigate(["/"]); + } else if (msg.command === "convertAccountToKeyConnector") { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router.navigate(["/remove-password"]); + } else if (msg.command === "switchAccountFinish") { + // TODO: unset loading? + // this.loading = false; + } else if (msg.command == "update-temp-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.router.navigate(["/update-temp-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.router.navigate(["home"]); - }); - this.changeDetectorRef.detectChanges(); - } else if (msg.command === "authBlocked") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["home"]); - } else if ( - msg.command === "locked" && - (msg.userId == null || msg.userId == this.activeUserId) - ) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["lock"]); - } else if (msg.command === "showDialog") { - // 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.showDialog(msg); - } else if (msg.command === "showNativeMessagingFinterprintDialog") { - // TODO: Should be refactored to live in another service. - // 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.showNativeMessagingFingerprintDialog(msg); - } else if (msg.command === "showToast") { - this.showToast(msg); - } else if (msg.command === "reloadProcess") { - const forceWindowReload = - this.platformUtilsService.isSafari() || - this.platformUtilsService.isFirefox() || - this.platformUtilsService.isOpera(); - // Wait to make sure background has reloaded first. - window.setTimeout( - () => BrowserApi.reloadExtension(forceWindowReload ? window : null), - 2000, - ); - } else if (msg.command === "reloadPopup") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/"]); - } else if (msg.command === "convertAccountToKeyConnector") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/remove-password"]); - } else if (msg.command === "switchAccountFinish") { - // TODO: unset loading? - // this.loading = false; - } else if (msg.command == "update-temp-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.router.navigate(["/update-temp-password"]); - } else { - msg.webExtSender = sender; - this.broadcasterService.send(msg); - } - }; - - (self as any).bitwardenPopupMainMessageListener = bitwardenPopupMainMessageListener; - this.browserMessagingApi.messageListener("app.component", bitwardenPopupMainMessageListener); + }), + takeUntil(this.destroy$), + ) + .subscribe(); // eslint-disable-next-line rxjs/no-async-subscribe this.router.events.pipe(takeUntil(this.destroy$)).subscribe(async (event) => { @@ -157,7 +159,7 @@ export class AppComponent implements OnInit, OnDestroy { await this.clearComponentStates(); } if (url.startsWith("/tabs/")) { - await this.stateService.setAddEditCipherInfo(null); + await this.cipherService.setAddEditCipherInfo(null); } (window as any).previousPopupUrl = url; @@ -231,8 +233,8 @@ export class AppComponent implements OnInit, OnDestroy { await Promise.all([ this.vaultBrowserStateService.setBrowserGroupingsComponentState(null), this.vaultBrowserStateService.setBrowserVaultItemsComponentState(null), - this.stateService.setBrowserSendComponentState(null), - this.stateService.setBrowserSendTypeComponentState(null), + this.browserSendStateService.setBrowserSendComponentState(null), + this.browserSendStateService.setBrowserSendTypeComponentState(null), ]); } } diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index d179868448..5718542b01 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -11,11 +11,10 @@ import { BrowserModule } from "@angular/platform-browser"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { BitwardenToastModule } from "@bitwarden/angular/components/toastr.component"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe"; import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe"; -import { AvatarModule, ButtonModule } from "@bitwarden/components"; +import { AvatarModule, ButtonModule, ToastModule } from "@bitwarden/components"; import { ExportScopeCalloutComponent } from "@bitwarden/vault-export-ui"; import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component"; @@ -87,7 +86,7 @@ import "../platform/popup/locales"; imports: [ A11yModule, AppRoutingModule, - BitwardenToastModule.forRoot({ + ToastModule.forRoot({ maxOpened: 2, autoDismiss: true, closeButton: true, diff --git a/apps/browser/src/popup/scss/plugins.scss b/apps/browser/src/popup/scss/plugins.scss deleted file mode 100644 index e1e386d62d..0000000000 --- a/apps/browser/src/popup/scss/plugins.scss +++ /dev/null @@ -1,98 +0,0 @@ -@import "~ngx-toastr/toastr"; - -@import "variables.scss"; -@import "buttons.scss"; - -// Toaster - -.toast-container { - .toast-close-button { - @include themify($themes) { - color: themed("toastTextColor"); - } - font-size: 18px; - margin-right: 4px; - } - - .ngx-toastr { - @include themify($themes) { - color: themed("toastTextColor"); - } - align-items: center; - background-image: none !important; - border-radius: $border-radius; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.35); - display: flex; - padding: 15px; - - .toast-close-button { - position: absolute; - right: 5px; - top: 0; - } - - &:hover { - box-shadow: 0 0 10px rgba(0, 0, 0, 0.6); - } - - .icon i::before { - float: left; - font-style: normal; - font-family: $icomoon-font-family; - font-size: 25px; - line-height: 20px; - padding-right: 15px; - } - - .toast-message { - p { - margin-bottom: 0.5rem; - - &:last-child { - margin-bottom: 0; - } - } - } - - &.toast-danger, - &.toast-error { - @include themify($themes) { - background-color: themed("dangerColor"); - } - - .icon i::before { - content: map_get($icons, "error"); - } - } - - &.toast-warning { - @include themify($themes) { - background-color: themed("warningColor"); - } - - .icon i::before { - content: map_get($icons, "exclamation-triangle"); - } - } - - &.toast-info { - @include themify($themes) { - background-color: themed("infoColor"); - } - - .icon i:before { - content: map_get($icons, "info-circle"); - } - } - - &.toast-success { - @include themify($themes) { - background-color: themed("successColor"); - } - - .icon i:before { - content: map_get($icons, "check"); - } - } - } -} diff --git a/apps/browser/src/popup/scss/popup.scss b/apps/browser/src/popup/scss/popup.scss index 0d7e428138..850ef96c64 100644 --- a/apps/browser/src/popup/scss/popup.scss +++ b/apps/browser/src/popup/scss/popup.scss @@ -8,7 +8,6 @@ @import "buttons.scss"; @import "misc.scss"; @import "modal.scss"; -@import "plugins.scss"; @import "environment.scss"; @import "pages.scss"; @import "@angular/cdk/overlay-prebuilt.css"; diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index 5c8266a46a..ee842565d7 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -9,7 +9,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { BrowserApi } from "../../platform/browser/browser-api"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; - @Injectable() export class InitService { constructor( diff --git a/apps/browser/src/popup/services/popup-search.service.ts b/apps/browser/src/popup/services/popup-search.service.ts index bc5e565e6c..40e6fd2d96 100644 --- a/apps/browser/src/popup/services/popup-search.service.ts +++ b/apps/browser/src/popup/services/popup-search.service.ts @@ -1,17 +1,14 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { StateProvider } from "@bitwarden/common/platform/state"; import { SearchService } from "@bitwarden/common/services/search.service"; export class PopupSearchService extends SearchService { - constructor( - private mainSearchService: SearchService, - logService: LogService, - i18nService: I18nService, - ) { - super(logService, i18nService); + constructor(logService: LogService, i18nService: I18nService, stateProvider: StateProvider) { + super(logService, i18nService, stateProvider); } - clearIndex() { + clearIndex(): Promise { throw new Error("Not available."); } @@ -19,7 +16,7 @@ export class PopupSearchService extends SearchService { throw new Error("Not available."); } - getIndexForSearch() { - return this.mainSearchService.getIndexForSearch(); + async getIndexForSearch() { + return await super.getIndexForSearch(); } } diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index e68873e31c..123e901e4e 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -1,7 +1,6 @@ import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core"; -import { DomSanitizer } from "@angular/platform-browser"; import { Router } from "@angular/router"; -import { ToastrService } from "ngx-toastr"; +import { Subject, merge } from "rxjs"; import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards"; import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service"; @@ -13,6 +12,7 @@ import { OBSERVABLE_MEMORY_STORAGE, SYSTEM_THEME_OBSERVABLE, SafeInjectionToken, + INTRAPROCESS_MESSAGING_SUBJECT, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { @@ -56,14 +56,17 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractMemoryStorageService, AbstractStorageService, + ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- Used for dependency injection +import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; @@ -74,17 +77,15 @@ import { GlobalStateProvider, StateProvider, } from "@bitwarden/common/platform/state"; -import { SearchService } from "@bitwarden/common/services/search.service"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; -import { CipherFileUploadService } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service"; import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; -import { DialogService } from "@bitwarden/components"; -import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; +import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; +import { TotpService } from "@bitwarden/common/vault/services/totp.service"; +import { DialogService, ToastService } from "@bitwarden/components"; import { UnauthGuardService } from "../../auth/popup/services"; import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service"; @@ -92,18 +93,24 @@ import AutofillService from "../../autofill/services/autofill.service"; import MainBackground from "../../background/main.background"; import { Account } from "../../models/account"; import { BrowserApi } from "../../platform/browser/browser-api"; +import { runInsideAngular } from "../../platform/browser/run-inside-angular.operator"; +/* eslint-disable no-restricted-imports */ +import { ChromeMessageSender } from "../../platform/messaging/chrome-message.sender"; +/* eslint-enable no-restricted-imports */ import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service"; import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.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 BrowserMessagingPrivateModePopupService from "../../platform/services/browser-messaging-private-mode-popup.service"; -import BrowserMessagingService from "../../platform/services/browser-messaging.service"; -import { BrowserStateService } from "../../platform/services/browser-state.service"; +import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; +import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service"; import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; +import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging"; +import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service"; import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service"; import { VaultBrowserStateService } from "../../vault/services/vault-browser-state.service"; import { VaultFilterService } from "../../vault/services/vault-filter.service"; @@ -120,7 +127,7 @@ const mainBackground: MainBackground = needsBackgroundInit : BrowserApi.getBackgroundPage().bitwardenMain; function createLocalBgService() { - const localBgService = new MainBackground(isPrivateMode); + const localBgService = new MainBackground(isPrivateMode, 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(); @@ -155,15 +162,6 @@ const safeProviders: SafeProvider[] = [ useClass: UnauthGuardService, deps: [AuthServiceAbstraction, Router], }), - safeProvider({ - provide: MessagingService, - useFactory: () => { - return needsBackgroundInit - ? new BrowserMessagingPrivateModePopupService() - : new BrowserMessagingService(); - }, - deps: [], - }), safeProvider({ provide: TwoFactorService, useFactory: getBgService("twoFactorService"), @@ -186,19 +184,8 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: SearchServiceAbstraction, - useFactory: (logService: LogService, i18nService: I18nServiceAbstraction) => { - return new PopupSearchService( - getBgService("searchService")(), - logService, - i18nService, - ); - }, - deps: [LogService, I18nServiceAbstraction], - }), - safeProvider({ - provide: CipherFileUploadService, - useFactory: getBgService("cipherFileUploadService"), - deps: [], + useClass: PopupSearchService, + deps: [LogService, I18nServiceAbstraction, StateProvider], }), safeProvider({ provide: CipherService, @@ -230,11 +217,6 @@ const safeProviders: SafeProvider[] = [ useClass: BrowserEnvironmentService, deps: [LogService, StateProvider, AccountServiceAbstraction], }), - safeProvider({ - provide: TotpService, - useFactory: getBgService("totpService"), - deps: [], - }), safeProvider({ provide: I18nServiceAbstraction, useFactory: (globalStateProvider: GlobalStateProvider) => { @@ -251,6 +233,11 @@ const safeProviders: SafeProvider[] = [ }, deps: [EncryptService], }), + safeProvider({ + provide: TotpServiceAbstraction, + useClass: TotpService, + deps: [CryptoFunctionService, LogService], + }), safeProvider({ provide: AuthRequestServiceAbstraction, useFactory: getBgService("authRequestService"), @@ -268,15 +255,9 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: PlatformUtilsService, - useExisting: ForegroundPlatformUtilsService, - }), - safeProvider({ - provide: ForegroundPlatformUtilsService, - useClass: ForegroundPlatformUtilsService, - useFactory: (sanitizer: DomSanitizer, toastrService: ToastrService) => { + useFactory: (toastService: ToastService) => { return new ForegroundPlatformUtilsService( - sanitizer, - toastrService, + toastService, (clipboardValue: string, clearMs: number) => { void BrowserApi.sendMessage("clearClipboard", { clipboardValue, clearMs }); }, @@ -293,7 +274,7 @@ const safeProviders: SafeProvider[] = [ window, ); }, - deps: [DomSanitizer, ToastrService], + deps: [ToastService], }), safeProvider({ provide: PasswordGenerationServiceAbstraction, @@ -324,17 +305,19 @@ const safeProviders: SafeProvider[] = [ deps: [ CipherService, AutofillSettingsServiceAbstraction, - TotpService, + TotpServiceAbstraction, EventCollectionServiceAbstraction, LogService, DomainSettingsService, UserVerificationService, BillingAccountProfileStateService, + ScriptInjectorService, + AccountServiceAbstraction, ], }), safeProvider({ - provide: VaultExportServiceAbstraction, - useFactory: getBgService("exportService"), + provide: ScriptInjectorService, + useClass: BrowserScriptInjectorService, deps: [], }), safeProvider({ @@ -386,7 +369,15 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: OBSERVABLE_MEMORY_STORAGE, - useClass: ForegroundMemoryStorageService, + useFactory: () => { + if (BrowserApi.isManifestVersion(2)) { + return new ForegroundMemoryStorageService(); + } + + return getBgService( + "memoryStorageForStateProviders", + )(); + }, deps: [], }), safeProvider({ @@ -412,7 +403,7 @@ const safeProviders: SafeProvider[] = [ tokenService: TokenService, migrationRunner: MigrationRunner, ) => { - return new BrowserStateService( + return new DefaultBrowserStateService( storageService, secureStorageService, memoryStorageService, @@ -487,6 +478,70 @@ const safeProviders: SafeProvider[] = [ useClass: UserNotificationSettingsService, deps: [StateProvider], }), + safeProvider({ + provide: BrowserSendStateService, + useClass: BrowserSendStateService, + deps: [StateProvider], + }), + safeProvider({ + provide: MessageListener, + useFactory: (subject: Subject>, ngZone: NgZone) => + new MessageListener( + merge( + subject.asObservable(), // For messages in the same context + fromChromeRuntimeMessaging().pipe(runInsideAngular(ngZone)), // For messages in the same context + ), + ), + deps: [INTRAPROCESS_MESSAGING_SUBJECT, NgZone], + }), + safeProvider({ + provide: MessageSender, + useFactory: (subject: Subject>, logService: LogService) => + MessageSender.combine( + new SubjectMessageSender(subject), // For sending messages in the same context + new ChromeMessageSender(logService), // For sending messages to different contexts + ), + deps: [INTRAPROCESS_MESSAGING_SUBJECT, LogService], + }), + 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>(); + } + }, + deps: [], + }), + safeProvider({ + provide: MessageSender, + useFactory: (subject: Subject>, logService: LogService) => + MessageSender.combine( + new SubjectMessageSender(subject), // For sending messages in the same context + new ChromeMessageSender(logService), // For sending messages to different contexts + ), + 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: [], + }), ]; @NgModule({ diff --git a/apps/browser/src/popup/settings/settings.component.html b/apps/browser/src/popup/settings/settings.component.html index f099528918..98c218b0db 100644 --- a/apps/browser/src/popup/settings/settings.component.html +++ b/apps/browser/src/popup/settings/settings.component.html @@ -153,7 +153,7 @@ *ngIf="showChangeMasterPass" >
{{ "changeMasterPassword" | i18n }}
- + diff --git a/apps/browser/src/tools/popup/send/send-groupings.component.ts b/apps/browser/src/tools/popup/send/send-groupings.component.ts index 25fa67d51a..87d03c4b76 100644 --- a/apps/browser/src/tools/popup/send/send-groupings.component.ts +++ b/apps/browser/src/tools/popup/send/send-groupings.component.ts @@ -18,7 +18,7 @@ import { DialogService } from "@bitwarden/components"; import { BrowserSendComponentState } from "../../../models/browserSendComponentState"; import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; -import { BrowserStateService } from "../../../platform/services/abstractions/browser-state.service"; +import { BrowserSendStateService } from "../services/browser-send-state.service"; const ComponentId = "SendComponent"; @@ -29,8 +29,6 @@ const ComponentId = "SendComponent"; export class SendGroupingsComponent extends BaseSendComponent { // Header showLeftHeader = true; - // Send Type Calculations - typeCounts = new Map(); // State Handling state: BrowserSendComponentState; private loadedTimeout: number; @@ -43,7 +41,7 @@ export class SendGroupingsComponent extends BaseSendComponent { ngZone: NgZone, policyService: PolicyService, searchService: SearchService, - private stateService: BrowserStateService, + private stateService: BrowserSendStateService, private router: Router, private syncService: SyncService, private changeDetectorRef: ChangeDetectorRef, @@ -65,7 +63,6 @@ export class SendGroupingsComponent extends BaseSendComponent { dialogService, ); super.onSuccessfulLoad = async () => { - this.calculateTypeCounts(); this.selectAll(); }; } @@ -171,22 +168,11 @@ export class SendGroupingsComponent extends BaseSendComponent { } showSearching() { - return ( - this.hasSearched || (!this.searchPending && this.searchService.isSearchable(this.searchText)) - ); + return this.hasSearched || (!this.searchPending && this.isSearchable); } - private calculateTypeCounts() { - // Create type counts - const typeCounts = new Map(); - this.sends.forEach((s) => { - if (typeCounts.has(s.type)) { - typeCounts.set(s.type, typeCounts.get(s.type) + 1); - } else { - typeCounts.set(s.type, 1); - } - }); - this.typeCounts = typeCounts; + getSendCount(sends: SendView[], type: SendType): number { + return sends.filter((s) => s.type === type).length; } private async saveState() { @@ -194,7 +180,6 @@ export class SendGroupingsComponent extends BaseSendComponent { scrollY: BrowserPopupUtils.getContentScrollY(window), searchText: this.searchText, sends: this.sends, - typeCounts: this.typeCounts, }); await this.stateService.setBrowserSendComponentState(this.state); } @@ -208,9 +193,6 @@ export class SendGroupingsComponent extends BaseSendComponent { if (this.state.sends != null) { this.sends = this.state.sends; } - if (this.state.typeCounts != null) { - this.typeCounts = this.state.typeCounts; - } return true; } diff --git a/apps/browser/src/tools/popup/send/send-type.component.ts b/apps/browser/src/tools/popup/send/send-type.component.ts index 4b27edc043..aca02587de 100644 --- a/apps/browser/src/tools/popup/send/send-type.component.ts +++ b/apps/browser/src/tools/popup/send/send-type.component.ts @@ -19,7 +19,7 @@ import { DialogService } from "@bitwarden/components"; import { BrowserComponentState } from "../../../models/browserComponentState"; import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; -import { BrowserStateService } from "../../../platform/services/abstractions/browser-state.service"; +import { BrowserSendStateService } from "../services/browser-send-state.service"; const ComponentId = "SendTypeComponent"; @@ -42,7 +42,7 @@ export class SendTypeComponent extends BaseSendComponent { ngZone: NgZone, policyService: PolicyService, searchService: SearchService, - private stateService: BrowserStateService, + private stateService: BrowserSendStateService, private route: ActivatedRoute, private location: Location, private changeDetectorRef: ChangeDetectorRef, diff --git a/apps/browser/src/tools/popup/services/browser-send-state.service.spec.ts b/apps/browser/src/tools/popup/services/browser-send-state.service.spec.ts new file mode 100644 index 0000000000..6f0ae1455a --- /dev/null +++ b/apps/browser/src/tools/popup/services/browser-send-state.service.spec.ts @@ -0,0 +1,59 @@ +import { + FakeAccountService, + mockAccountServiceWith, +} from "@bitwarden/common/../spec/fake-account-service"; +import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider"; +import { awaitAsync } from "@bitwarden/common/../spec/utils"; + +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { UserId } from "@bitwarden/common/types/guid"; + +import { BrowserComponentState } from "../../../models/browserComponentState"; +import { BrowserSendComponentState } from "../../../models/browserSendComponentState"; + +import { BrowserSendStateService } from "./browser-send-state.service"; + +describe("Browser Send State Service", () => { + let stateProvider: FakeStateProvider; + + let accountService: FakeAccountService; + let stateService: BrowserSendStateService; + const mockUserId = Utils.newGuid() as UserId; + + beforeEach(() => { + accountService = mockAccountServiceWith(mockUserId); + stateProvider = new FakeStateProvider(accountService); + + stateService = new BrowserSendStateService(stateProvider); + }); + + describe("getBrowserSendComponentState", () => { + it("should return BrowserSendComponentState", async () => { + const state = new BrowserSendComponentState(); + state.scrollY = 0; + state.searchText = "test"; + + await stateService.setBrowserSendComponentState(state); + + await awaitAsync(); + + const actual = await stateService.getBrowserSendComponentState(); + expect(actual).toStrictEqual(state); + }); + }); + + describe("getBrowserSendTypeComponentState", () => { + it("should return BrowserComponentState", async () => { + const state = new BrowserComponentState(); + state.scrollY = 0; + state.searchText = "test"; + + await stateService.setBrowserSendTypeComponentState(state); + + await awaitAsync(); + + const actual = await stateService.getBrowserSendTypeComponentState(); + expect(actual).toStrictEqual(state); + }); + }); +}); diff --git a/apps/browser/src/tools/popup/services/browser-send-state.service.ts b/apps/browser/src/tools/popup/services/browser-send-state.service.ts new file mode 100644 index 0000000000..52aeb01a92 --- /dev/null +++ b/apps/browser/src/tools/popup/services/browser-send-state.service.ts @@ -0,0 +1,65 @@ +import { Observable, firstValueFrom } from "rxjs"; + +import { ActiveUserState, StateProvider } from "@bitwarden/common/platform/state"; + +import { BrowserComponentState } from "../../../models/browserComponentState"; +import { BrowserSendComponentState } from "../../../models/browserSendComponentState"; + +import { BROWSER_SEND_COMPONENT, BROWSER_SEND_TYPE_COMPONENT } from "./key-definitions"; + +/** Get or set the active user's component state for the Send browser component + */ +export class BrowserSendStateService { + /** Observable that contains the current state for active user Sends including the send data and type counts + * along with the search text and scroll position + */ + browserSendComponentState$: Observable; + + /** Observable that contains the current state for active user Sends that only includes the search text + * and scroll position + */ + browserSendTypeComponentState$: Observable; + + private activeUserBrowserSendComponentState: ActiveUserState; + private activeUserBrowserSendTypeComponentState: ActiveUserState; + + constructor(protected stateProvider: StateProvider) { + this.activeUserBrowserSendComponentState = this.stateProvider.getActive(BROWSER_SEND_COMPONENT); + this.browserSendComponentState$ = this.activeUserBrowserSendComponentState.state$; + + this.activeUserBrowserSendTypeComponentState = this.stateProvider.getActive( + BROWSER_SEND_TYPE_COMPONENT, + ); + this.browserSendTypeComponentState$ = this.activeUserBrowserSendTypeComponentState.state$; + } + + /** Get the active user's browser send component state + * @returns { BrowserSendComponentState } contains the sends and type counts along with the scroll position and search text for the + * send component on the browser + */ + async getBrowserSendComponentState(): Promise { + return await firstValueFrom(this.browserSendComponentState$); + } + + /** Set the active user's browser send component state + * @param { BrowserSendComponentState } value sets the sends along with the scroll position and search text for + * the send component on the browser + */ + async setBrowserSendComponentState(value: BrowserSendComponentState): Promise { + await this.activeUserBrowserSendComponentState.update(() => value); + } + + /** Get the active user's browser component state + * @returns { BrowserComponentState } contains the scroll position and search text for the sends menu on the browser + */ + async getBrowserSendTypeComponentState(): Promise { + return await firstValueFrom(this.browserSendTypeComponentState$); + } + + /** Set the active user's browser component state + * @param { BrowserComponentState } value set the scroll position and search text for the send component on the browser + */ + async setBrowserSendTypeComponentState(value: BrowserComponentState): Promise { + await this.activeUserBrowserSendTypeComponentState.update(() => value); + } +} diff --git a/apps/browser/src/tools/popup/services/key-definitions.spec.ts b/apps/browser/src/tools/popup/services/key-definitions.spec.ts new file mode 100644 index 0000000000..7517771669 --- /dev/null +++ b/apps/browser/src/tools/popup/services/key-definitions.spec.ts @@ -0,0 +1,39 @@ +import { Jsonify } from "type-fest"; + +import { BrowserSendComponentState } from "../../../models/browserSendComponentState"; + +import { BROWSER_SEND_COMPONENT, BROWSER_SEND_TYPE_COMPONENT } from "./key-definitions"; + +describe("Key definitions", () => { + describe("BROWSER_SEND_COMPONENT", () => { + it("should deserialize BrowserSendComponentState", () => { + const keyDef = BROWSER_SEND_COMPONENT; + + const expectedState = { + scrollY: 0, + searchText: "test", + }; + + const result = keyDef.deserializer( + JSON.parse(JSON.stringify(expectedState)) as Jsonify, + ); + + expect(result).toEqual(expectedState); + }); + }); + + describe("BROWSER_SEND_TYPE_COMPONENT", () => { + it("should deserialize BrowserComponentState", () => { + const keyDef = BROWSER_SEND_TYPE_COMPONENT; + + const expectedState = { + scrollY: 0, + searchText: "test", + }; + + const result = keyDef.deserializer(JSON.parse(JSON.stringify(expectedState))); + + expect(result).toEqual(expectedState); + }); + }); +}); diff --git a/apps/browser/src/tools/popup/services/key-definitions.ts b/apps/browser/src/tools/popup/services/key-definitions.ts new file mode 100644 index 0000000000..9b256073f3 --- /dev/null +++ b/apps/browser/src/tools/popup/services/key-definitions.ts @@ -0,0 +1,23 @@ +import { Jsonify } from "type-fest"; + +import { BROWSER_SEND_MEMORY, KeyDefinition } from "@bitwarden/common/platform/state"; + +import { BrowserComponentState } from "../../../models/browserComponentState"; +import { BrowserSendComponentState } from "../../../models/browserSendComponentState"; + +export const BROWSER_SEND_COMPONENT = new KeyDefinition( + BROWSER_SEND_MEMORY, + "browser_send_component", + { + deserializer: (obj: Jsonify) => + BrowserSendComponentState.fromJSON(obj), + }, +); + +export const BROWSER_SEND_TYPE_COMPONENT = new KeyDefinition( + BROWSER_SEND_MEMORY, + "browser_send_type_component", + { + deserializer: (obj: Jsonify) => BrowserComponentState.fromJSON(obj), + }, +); diff --git a/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts b/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts index 8ffeca72bc..57366ea8c0 100644 --- a/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts +++ b/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts @@ -42,6 +42,7 @@ import { i18nServiceFactory, I18nServiceInitOptions, } from "../../../platform/background/service-factories/i18n-service.factory"; +import { stateProviderFactory } from "../../../platform/background/service-factories/state-provider.factory"; import { stateServiceFactory, StateServiceInitOptions, @@ -81,6 +82,7 @@ export function cipherServiceFactory( await encryptServiceFactory(cache, opts), await cipherFileUploadServiceFactory(cache, opts), await configServiceFactory(cache, opts), + await stateProviderFactory(cache, opts), ), ); } diff --git a/apps/browser/src/vault/background/service_factories/folder-service.factory.ts b/apps/browser/src/vault/background/service_factories/folder-service.factory.ts index 72847a0536..0593dc904c 100644 --- a/apps/browser/src/vault/background/service_factories/folder-service.factory.ts +++ b/apps/browser/src/vault/background/service_factories/folder-service.factory.ts @@ -14,11 +14,10 @@ import { i18nServiceFactory, I18nServiceInitOptions, } from "../../../platform/background/service-factories/i18n-service.factory"; -import { stateProviderFactory } from "../../../platform/background/service-factories/state-provider.factory"; import { - stateServiceFactory as stateServiceFactory, - StateServiceInitOptions, -} from "../../../platform/background/service-factories/state-service.factory"; + stateProviderFactory, + StateProviderInitOptions, +} from "../../../platform/background/service-factories/state-provider.factory"; import { cipherServiceFactory, CipherServiceInitOptions } from "./cipher-service.factory"; @@ -28,7 +27,7 @@ export type FolderServiceInitOptions = FolderServiceFactoryOptions & CryptoServiceInitOptions & CipherServiceInitOptions & I18nServiceInitOptions & - StateServiceInitOptions; + StateProviderInitOptions; export function folderServiceFactory( cache: { folderService?: AbstractFolderService } & CachedServices, @@ -43,7 +42,6 @@ export function folderServiceFactory( await cryptoServiceFactory(cache, opts), await i18nServiceFactory(cache, opts), await cipherServiceFactory(cache, opts), - await stateServiceFactory(cache, opts), await stateProviderFactory(cache, opts), ), ); diff --git a/apps/browser/src/vault/fido2/background/abstractions/fido2.background.ts b/apps/browser/src/vault/fido2/background/abstractions/fido2.background.ts new file mode 100644 index 0000000000..49f248c7b8 --- /dev/null +++ b/apps/browser/src/vault/fido2/background/abstractions/fido2.background.ts @@ -0,0 +1,57 @@ +import { + AssertCredentialParams, + AssertCredentialResult, + CreateCredentialParams, + CreateCredentialResult, +} from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction"; + +type SharedFido2ScriptInjectionDetails = { + runAt: browser.contentScripts.RegisteredContentScriptOptions["runAt"]; +}; + +type SharedFido2ScriptRegistrationOptions = SharedFido2ScriptInjectionDetails & { + matches: string[]; + excludeMatches: string[]; + allFrames: true; +}; + +type Fido2ExtensionMessage = { + [key: string]: any; + command: string; + hostname?: string; + origin?: string; + requestId?: string; + abortedRequestId?: string; + data?: AssertCredentialParams | CreateCredentialParams; +}; + +type Fido2ExtensionMessageEventParams = { + message: Fido2ExtensionMessage; + sender: chrome.runtime.MessageSender; +}; + +type Fido2BackgroundExtensionMessageHandlers = { + [key: string]: CallableFunction; + fido2AbortRequest: ({ message }: Fido2ExtensionMessageEventParams) => void; + fido2RegisterCredentialRequest: ({ + message, + sender, + }: Fido2ExtensionMessageEventParams) => Promise; + fido2GetCredentialRequest: ({ + message, + sender, + }: Fido2ExtensionMessageEventParams) => Promise; +}; + +interface Fido2Background { + init(): void; + injectFido2ContentScriptsInAllTabs(): Promise; +} + +export { + SharedFido2ScriptInjectionDetails, + SharedFido2ScriptRegistrationOptions, + Fido2ExtensionMessage, + Fido2BackgroundExtensionMessageHandlers, + Fido2Background, +}; diff --git a/apps/browser/src/vault/fido2/background/fido2.background.spec.ts b/apps/browser/src/vault/fido2/background/fido2.background.spec.ts new file mode 100644 index 0000000000..534d8a99c5 --- /dev/null +++ b/apps/browser/src/vault/fido2/background/fido2.background.spec.ts @@ -0,0 +1,414 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { + AssertCredentialParams, + CreateCredentialParams, +} from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction"; +import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; +import { Fido2ClientService } from "@bitwarden/common/vault/services/fido2/fido2-client.service"; + +import { createPortSpyMock } from "../../../autofill/spec/autofill-mocks"; +import { + flushPromises, + sendExtensionRuntimeMessage, + triggerPortOnDisconnectEvent, + triggerRuntimeOnConnectEvent, +} from "../../../autofill/spec/testing-utils"; +import { BrowserApi } from "../../../platform/browser/browser-api"; +import { BrowserScriptInjectorService } from "../../../platform/services/browser-script-injector.service"; +import { AbortManager } from "../../background/abort-manager"; +import { Fido2ContentScript, Fido2ContentScriptId } from "../enums/fido2-content-script.enum"; +import { Fido2PortName } from "../enums/fido2-port-name.enum"; + +import { Fido2ExtensionMessage } from "./abstractions/fido2.background"; +import { Fido2Background } from "./fido2.background"; + +const sharedExecuteScriptOptions = { runAt: "document_start" }; +const sharedScriptInjectionDetails = { frame: "all_frames", ...sharedExecuteScriptOptions }; +const contentScriptDetails = { + file: Fido2ContentScript.ContentScript, + ...sharedScriptInjectionDetails, +}; +const sharedRegistrationOptions = { + matches: ["https://*/*"], + excludeMatches: ["https://*/*.xml*"], + allFrames: true, + ...sharedExecuteScriptOptions, +}; + +describe("Fido2Background", () => { + const tabsQuerySpy: jest.SpyInstance = jest.spyOn(BrowserApi, "tabsQuery"); + const isManifestVersionSpy: jest.SpyInstance = jest.spyOn(BrowserApi, "isManifestVersion"); + const focusTabSpy: jest.SpyInstance = jest.spyOn(BrowserApi, "focusTab").mockResolvedValue(); + const focusWindowSpy: jest.SpyInstance = jest + .spyOn(BrowserApi, "focusWindow") + .mockResolvedValue(); + let abortManagerMock!: MockProxy; + let abortController!: MockProxy; + let registeredContentScripsMock!: MockProxy; + let tabMock!: MockProxy; + let senderMock!: MockProxy; + let logService!: MockProxy; + let fido2ClientService!: MockProxy; + let vaultSettingsService!: MockProxy; + let scriptInjectorServiceMock!: MockProxy; + let enablePasskeysMock$!: BehaviorSubject; + let fido2Background!: Fido2Background; + + beforeEach(() => { + tabMock = mock({ + id: 123, + url: "https://example.com", + windowId: 456, + }); + senderMock = mock({ id: "1", tab: tabMock }); + logService = mock(); + fido2ClientService = mock(); + vaultSettingsService = mock(); + abortManagerMock = mock(); + abortController = mock(); + registeredContentScripsMock = mock(); + scriptInjectorServiceMock = mock(); + + enablePasskeysMock$ = new BehaviorSubject(true); + vaultSettingsService.enablePasskeys$ = enablePasskeysMock$; + fido2ClientService.isFido2FeatureEnabled.mockResolvedValue(true); + fido2Background = new Fido2Background( + logService, + fido2ClientService, + vaultSettingsService, + scriptInjectorServiceMock, + ); + fido2Background["abortManager"] = abortManagerMock; + abortManagerMock.runWithAbortController.mockImplementation((_requestId, runner) => + runner(abortController), + ); + isManifestVersionSpy.mockImplementation((manifestVersion) => manifestVersion === 2); + }); + + afterEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }); + + describe("injectFido2ContentScriptsInAllTabs", () => { + it("does not inject any FIDO2 content scripts when no tabs have a secure url protocol", async () => { + const insecureTab = mock({ id: 789, url: "http://example.com" }); + tabsQuerySpy.mockResolvedValueOnce([insecureTab]); + + await fido2Background.injectFido2ContentScriptsInAllTabs(); + + expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalled(); + }); + + it("only injects the FIDO2 content script into tabs that contain a secure url protocol", async () => { + const secondTabMock = mock({ id: 456, url: "https://example.com" }); + const insecureTab = mock({ id: 789, url: "http://example.com" }); + const noUrlTab = mock({ id: 101, url: undefined }); + tabsQuerySpy.mockResolvedValueOnce([tabMock, secondTabMock, insecureTab, noUrlTab]); + + await fido2Background.injectFido2ContentScriptsInAllTabs(); + + expect(scriptInjectorServiceMock.inject).toHaveBeenCalledWith({ + tabId: tabMock.id, + injectDetails: contentScriptDetails, + }); + expect(scriptInjectorServiceMock.inject).toHaveBeenCalledWith({ + tabId: secondTabMock.id, + injectDetails: contentScriptDetails, + }); + expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalledWith({ + tabId: insecureTab.id, + injectDetails: contentScriptDetails, + }); + expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalledWith({ + tabId: noUrlTab.id, + injectDetails: contentScriptDetails, + }); + }); + + it("injects the `page-script.js` content script into the provided tab", async () => { + tabsQuerySpy.mockResolvedValueOnce([tabMock]); + + await fido2Background.injectFido2ContentScriptsInAllTabs(); + + expect(scriptInjectorServiceMock.inject).toHaveBeenCalledWith({ + tabId: tabMock.id, + injectDetails: sharedScriptInjectionDetails, + mv2Details: { file: Fido2ContentScript.PageScriptAppend }, + mv3Details: { file: Fido2ContentScript.PageScript, world: "MAIN" }, + }); + }); + }); + + describe("handleEnablePasskeysUpdate", () => { + let portMock!: MockProxy; + + beforeEach(() => { + fido2Background.init(); + jest.spyOn(BrowserApi, "registerContentScriptsMv2"); + jest.spyOn(BrowserApi, "registerContentScriptsMv3"); + jest.spyOn(BrowserApi, "unregisterContentScriptsMv3"); + portMock = createPortSpyMock(Fido2PortName.InjectedScript); + triggerRuntimeOnConnectEvent(portMock); + triggerRuntimeOnConnectEvent(createPortSpyMock("some-other-port")); + + tabsQuerySpy.mockResolvedValue([tabMock]); + }); + + it("does not destroy and re-inject the content scripts when triggering `handleEnablePasskeysUpdate` with an undefined currentEnablePasskeysSetting property", async () => { + await flushPromises(); + + expect(portMock.disconnect).not.toHaveBeenCalled(); + expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalled(); + }); + + it("destroys the content scripts but skips re-injecting them when the enablePasskeys setting is set to `false`", async () => { + enablePasskeysMock$.next(false); + await flushPromises(); + + expect(portMock.disconnect).toHaveBeenCalled(); + expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalled(); + }); + + it("destroys and re-injects the content scripts when the enablePasskeys setting is set to `true`", async () => { + enablePasskeysMock$.next(true); + await flushPromises(); + + expect(portMock.disconnect).toHaveBeenCalled(); + expect(scriptInjectorServiceMock.inject).toHaveBeenCalledWith({ + tabId: tabMock.id, + injectDetails: sharedScriptInjectionDetails, + mv2Details: { file: Fido2ContentScript.PageScriptAppend }, + mv3Details: { file: Fido2ContentScript.PageScript, world: "MAIN" }, + }); + expect(scriptInjectorServiceMock.inject).toHaveBeenCalledWith({ + tabId: tabMock.id, + injectDetails: contentScriptDetails, + }); + }); + + describe("given manifest v2", () => { + it("registers the page-script-append-mv2.js and content-script.js content scripts when the enablePasskeys setting is set to `true`", async () => { + isManifestVersionSpy.mockImplementation((manifestVersion) => manifestVersion === 2); + + enablePasskeysMock$.next(true); + await flushPromises(); + + expect(BrowserApi.registerContentScriptsMv2).toHaveBeenCalledWith({ + js: [ + { file: Fido2ContentScript.PageScriptAppend }, + { file: Fido2ContentScript.ContentScript }, + ], + ...sharedRegistrationOptions, + }); + }); + + it("unregisters any existing registered content scripts when the enablePasskeys setting is set to `false`", async () => { + isManifestVersionSpy.mockImplementation((manifestVersion) => manifestVersion === 2); + fido2Background["registeredContentScripts"] = registeredContentScripsMock; + + enablePasskeysMock$.next(false); + await flushPromises(); + + expect(registeredContentScripsMock.unregister).toHaveBeenCalled(); + expect(BrowserApi.registerContentScriptsMv2).not.toHaveBeenCalledTimes(2); + }); + }); + + describe("given manifest v3", () => { + it("registers the page-script.js and content-script.js content scripts when the enablePasskeys setting is set to `true`", async () => { + isManifestVersionSpy.mockImplementation((manifestVersion) => manifestVersion === 3); + + enablePasskeysMock$.next(true); + await flushPromises(); + + expect(BrowserApi.registerContentScriptsMv3).toHaveBeenCalledWith([ + { + id: Fido2ContentScriptId.PageScript, + js: [Fido2ContentScript.PageScript], + world: "MAIN", + ...sharedRegistrationOptions, + }, + { + id: Fido2ContentScriptId.ContentScript, + js: [Fido2ContentScript.ContentScript], + ...sharedRegistrationOptions, + }, + ]); + expect(BrowserApi.unregisterContentScriptsMv3).not.toHaveBeenCalled(); + }); + + it("unregisters the page-script.js and content-script.js content scripts when the enablePasskeys setting is set to `false`", async () => { + isManifestVersionSpy.mockImplementation((manifestVersion) => manifestVersion === 3); + + enablePasskeysMock$.next(false); + await flushPromises(); + + expect(BrowserApi.unregisterContentScriptsMv3).toHaveBeenCalledWith({ + ids: [Fido2ContentScriptId.PageScript, Fido2ContentScriptId.ContentScript], + }); + expect(BrowserApi.registerContentScriptsMv3).not.toHaveBeenCalledTimes(2); + }); + }); + }); + + describe("extension message handlers", () => { + beforeEach(() => { + fido2Background.init(); + }); + + it("ignores messages that do not have a handler associated with a command within the message", () => { + const message = mock({ command: "nonexistentCommand" }); + + sendExtensionRuntimeMessage(message); + + expect(abortManagerMock.abort).not.toHaveBeenCalled(); + }); + + it("sends a response for rejected promises returned by a handler", async () => { + const message = mock({ command: "fido2RegisterCredentialRequest" }); + const sender = mock(); + const sendResponse = jest.fn(); + fido2ClientService.createCredential.mockRejectedValue(new Error("error")); + + sendExtensionRuntimeMessage(message, sender, sendResponse); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith({ error: { message: "error" } }); + }); + + describe("fido2AbortRequest message", () => { + it("aborts the request associated with the passed abortedRequestId", async () => { + const message = mock({ + command: "fido2AbortRequest", + abortedRequestId: "123", + }); + + sendExtensionRuntimeMessage(message); + await flushPromises(); + + expect(abortManagerMock.abort).toHaveBeenCalledWith(message.abortedRequestId); + }); + }); + + describe("fido2RegisterCredentialRequest message", () => { + it("creates a credential within the Fido2ClientService", async () => { + const message = mock({ + command: "fido2RegisterCredentialRequest", + requestId: "123", + data: mock(), + }); + + sendExtensionRuntimeMessage(message, senderMock); + await flushPromises(); + + expect(fido2ClientService.createCredential).toHaveBeenCalledWith( + message.data, + tabMock, + abortController, + ); + expect(focusTabSpy).toHaveBeenCalledWith(tabMock.id); + expect(focusWindowSpy).toHaveBeenCalledWith(tabMock.windowId); + }); + }); + + describe("fido2GetCredentialRequest", () => { + it("asserts a credential within the Fido2ClientService", async () => { + const message = mock({ + command: "fido2GetCredentialRequest", + requestId: "123", + data: mock(), + }); + + sendExtensionRuntimeMessage(message, senderMock); + await flushPromises(); + + expect(fido2ClientService.assertCredential).toHaveBeenCalledWith( + message.data, + tabMock, + abortController, + ); + expect(focusTabSpy).toHaveBeenCalledWith(tabMock.id); + expect(focusWindowSpy).toHaveBeenCalledWith(tabMock.windowId); + }); + }); + }); + + describe("handle ports onConnect", () => { + let portMock!: MockProxy; + + beforeEach(() => { + fido2Background.init(); + portMock = createPortSpyMock(Fido2PortName.InjectedScript); + fido2ClientService.isFido2FeatureEnabled.mockResolvedValue(true); + }); + + it("ignores port connections that do not have the correct port name", async () => { + const port = createPortSpyMock("nonexistentPort"); + + triggerRuntimeOnConnectEvent(port); + await flushPromises(); + + expect(port.onDisconnect.addListener).not.toHaveBeenCalled(); + }); + + it("ignores port connections that do not have a sender url", async () => { + portMock.sender = undefined; + + triggerRuntimeOnConnectEvent(portMock); + await flushPromises(); + + expect(portMock.onDisconnect.addListener).not.toHaveBeenCalled(); + }); + + it("disconnects the port connection when the Fido2 feature is not enabled", async () => { + fido2ClientService.isFido2FeatureEnabled.mockResolvedValue(false); + + triggerRuntimeOnConnectEvent(portMock); + await flushPromises(); + + expect(portMock.disconnect).toHaveBeenCalled(); + }); + + it("disconnects the port connection when the url is malformed", async () => { + portMock.sender.url = "malformed-url"; + + triggerRuntimeOnConnectEvent(portMock); + await flushPromises(); + + expect(portMock.disconnect).toHaveBeenCalled(); + expect(logService.error).toHaveBeenCalled(); + }); + + it("adds the port to the fido2ContentScriptPortsSet when the Fido2 feature is enabled", async () => { + triggerRuntimeOnConnectEvent(portMock); + await flushPromises(); + + expect(portMock.onDisconnect.addListener).toHaveBeenCalled(); + }); + }); + + describe("handleInjectScriptPortOnDisconnect", () => { + let portMock!: MockProxy; + + beforeEach(() => { + fido2Background.init(); + portMock = createPortSpyMock(Fido2PortName.InjectedScript); + triggerRuntimeOnConnectEvent(portMock); + fido2Background["fido2ContentScriptPortsSet"].add(portMock); + }); + + it("does not destroy or inject the content script when the port has already disconnected before the enablePasskeys setting is set to `false`", async () => { + triggerPortOnDisconnectEvent(portMock); + + enablePasskeysMock$.next(false); + await flushPromises(); + + expect(portMock.disconnect).not.toHaveBeenCalled(); + expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/vault/fido2/background/fido2.background.ts b/apps/browser/src/vault/fido2/background/fido2.background.ts new file mode 100644 index 0000000000..856874cee3 --- /dev/null +++ b/apps/browser/src/vault/fido2/background/fido2.background.ts @@ -0,0 +1,356 @@ +import { firstValueFrom, startWith } from "rxjs"; +import { pairwise } from "rxjs/operators"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { + AssertCredentialParams, + AssertCredentialResult, + CreateCredentialParams, + CreateCredentialResult, + Fido2ClientService, +} from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction"; +import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; +import { ScriptInjectorService } from "../../../platform/services/abstractions/script-injector.service"; +import { AbortManager } from "../../background/abort-manager"; +import { Fido2ContentScript, Fido2ContentScriptId } from "../enums/fido2-content-script.enum"; +import { Fido2PortName } from "../enums/fido2-port-name.enum"; + +import { + Fido2Background as Fido2BackgroundInterface, + Fido2BackgroundExtensionMessageHandlers, + Fido2ExtensionMessage, + SharedFido2ScriptInjectionDetails, + SharedFido2ScriptRegistrationOptions, +} from "./abstractions/fido2.background"; + +export class Fido2Background implements Fido2BackgroundInterface { + private abortManager = new AbortManager(); + private fido2ContentScriptPortsSet = new Set(); + private registeredContentScripts: browser.contentScripts.RegisteredContentScript; + private readonly sharedInjectionDetails: SharedFido2ScriptInjectionDetails = { + runAt: "document_start", + }; + private readonly sharedRegistrationOptions: SharedFido2ScriptRegistrationOptions = { + matches: ["https://*/*"], + excludeMatches: ["https://*/*.xml*"], + allFrames: true, + ...this.sharedInjectionDetails, + }; + private readonly extensionMessageHandlers: Fido2BackgroundExtensionMessageHandlers = { + fido2AbortRequest: ({ message }) => this.abortRequest(message), + fido2RegisterCredentialRequest: ({ message, sender }) => + this.registerCredentialRequest(message, sender), + fido2GetCredentialRequest: ({ message, sender }) => this.getCredentialRequest(message, sender), + }; + + constructor( + private logService: LogService, + private fido2ClientService: Fido2ClientService, + private vaultSettingsService: VaultSettingsService, + private scriptInjectorService: ScriptInjectorService, + ) {} + + /** + * Initializes the FIDO2 background service. Sets up the extension message + * and port listeners. Subscribes to the enablePasskeys$ observable to + * handle passkey enable/disable events. + */ + init() { + BrowserApi.messageListener("fido2.background", this.handleExtensionMessage); + BrowserApi.addListener(chrome.runtime.onConnect, this.handleInjectedScriptPortConnection); + this.vaultSettingsService.enablePasskeys$ + .pipe(startWith(undefined), pairwise()) + .subscribe(([previous, current]) => this.handleEnablePasskeysUpdate(previous, current)); + } + + /** + * Injects the FIDO2 content and page script into all existing browser tabs. + */ + async injectFido2ContentScriptsInAllTabs() { + const tabs = await BrowserApi.tabsQuery({}); + for (let index = 0; index < tabs.length; index++) { + const tab = tabs[index]; + if (!tab.url?.startsWith("https")) { + continue; + } + + void this.injectFido2ContentScripts(tab); + } + } + + /** + * Handles reacting to the enablePasskeys setting being updated. If the setting + * is enabled, the FIDO2 content scripts are injected into all tabs. If the setting + * is disabled, the FIDO2 content scripts will be from all tabs. This logic will + * not trigger until after the first setting update. + * + * @param previousEnablePasskeysSetting - The previous value of the enablePasskeys setting. + * @param enablePasskeys - The new value of the enablePasskeys setting. + */ + private async handleEnablePasskeysUpdate( + previousEnablePasskeysSetting: boolean, + enablePasskeys: boolean, + ) { + await this.updateContentScriptRegistration(); + + if (previousEnablePasskeysSetting === undefined) { + return; + } + + this.destroyLoadedFido2ContentScripts(); + if (enablePasskeys) { + void this.injectFido2ContentScriptsInAllTabs(); + } + } + + /** + * Updates the registration status of static FIDO2 content + * scripts based on the enablePasskeys setting. + */ + private async updateContentScriptRegistration() { + if (BrowserApi.isManifestVersion(2)) { + await this.updateMv2ContentScriptsRegistration(); + + return; + } + + await this.updateMv3ContentScriptsRegistration(); + } + + /** + * Updates the registration status of static FIDO2 content + * scripts based on the enablePasskeys setting for manifest v2. + */ + private async updateMv2ContentScriptsRegistration() { + if (!(await this.isPasskeySettingEnabled())) { + await this.registeredContentScripts?.unregister(); + + return; + } + + this.registeredContentScripts = await BrowserApi.registerContentScriptsMv2({ + js: [ + { file: Fido2ContentScript.PageScriptAppend }, + { file: Fido2ContentScript.ContentScript }, + ], + ...this.sharedRegistrationOptions, + }); + } + + /** + * Updates the registration status of static FIDO2 content + * scripts based on the enablePasskeys setting for manifest v3. + */ + private async updateMv3ContentScriptsRegistration() { + if (await this.isPasskeySettingEnabled()) { + void BrowserApi.registerContentScriptsMv3([ + { + id: Fido2ContentScriptId.PageScript, + js: [Fido2ContentScript.PageScript], + world: "MAIN", + ...this.sharedRegistrationOptions, + }, + { + id: Fido2ContentScriptId.ContentScript, + js: [Fido2ContentScript.ContentScript], + ...this.sharedRegistrationOptions, + }, + ]); + + return; + } + + void BrowserApi.unregisterContentScriptsMv3({ + ids: [Fido2ContentScriptId.PageScript, Fido2ContentScriptId.ContentScript], + }); + } + + /** + * Injects the FIDO2 content and page script into the current tab. + * + * @param tab - The current tab to inject the scripts into. + */ + private async injectFido2ContentScripts(tab: chrome.tabs.Tab): Promise { + void this.scriptInjectorService.inject({ + tabId: tab.id, + injectDetails: { frame: "all_frames", ...this.sharedInjectionDetails }, + mv2Details: { file: Fido2ContentScript.PageScriptAppend }, + mv3Details: { file: Fido2ContentScript.PageScript, world: "MAIN" }, + }); + + void this.scriptInjectorService.inject({ + tabId: tab.id, + injectDetails: { + file: Fido2ContentScript.ContentScript, + frame: "all_frames", + ...this.sharedInjectionDetails, + }, + }); + } + + /** + * Iterates over the set of injected FIDO2 content script ports + * and disconnects them, destroying the content scripts. + */ + private destroyLoadedFido2ContentScripts() { + this.fido2ContentScriptPortsSet.forEach((port) => { + port.disconnect(); + this.fido2ContentScriptPortsSet.delete(port); + }); + } + + /** + * Aborts the FIDO2 request with the provided requestId. + * + * @param message - The FIDO2 extension message containing the requestId to abort. + */ + private abortRequest(message: Fido2ExtensionMessage) { + this.abortManager.abort(message.abortedRequestId); + } + + /** + * Registers a new FIDO2 credential with the provided request data. + * + * @param message - The FIDO2 extension message containing the request data. + * @param sender - The sender of the message. + */ + private async registerCredentialRequest( + message: Fido2ExtensionMessage, + sender: chrome.runtime.MessageSender, + ): Promise { + return await this.handleCredentialRequest( + message, + sender.tab, + this.fido2ClientService.createCredential.bind(this.fido2ClientService), + ); + } + + /** + * Gets a FIDO2 credential with the provided request data. + * + * @param message - The FIDO2 extension message containing the request data. + * @param sender - The sender of the message. + */ + private async getCredentialRequest( + message: Fido2ExtensionMessage, + sender: chrome.runtime.MessageSender, + ): Promise { + return await this.handleCredentialRequest( + message, + sender.tab, + this.fido2ClientService.assertCredential.bind(this.fido2ClientService), + ); + } + + /** + * Handles Fido2 credential requests by calling the provided callback with the + * request data, tab, and abort controller. The callback is expected to return + * a promise that resolves with the result of the credential request. + * + * @param requestId - The request ID associated with the request. + * @param data - The request data to handle. + * @param tab - The tab associated with the request. + * @param callback - The callback to call with the request data, tab, and abort controller. + */ + private handleCredentialRequest = async ( + { requestId, data }: Fido2ExtensionMessage, + tab: chrome.tabs.Tab, + callback: ( + data: AssertCredentialParams | CreateCredentialParams, + tab: chrome.tabs.Tab, + abortController: AbortController, + ) => Promise, + ) => { + return await this.abortManager.runWithAbortController(requestId, async (abortController) => { + try { + return await callback(data, tab, abortController); + } finally { + await BrowserApi.focusTab(tab.id); + await BrowserApi.focusWindow(tab.windowId); + } + }); + }; + + /** + * Checks if the enablePasskeys setting is enabled. + */ + private async isPasskeySettingEnabled() { + return await firstValueFrom(this.vaultSettingsService.enablePasskeys$); + } + + /** + * Handles the FIDO2 extension message by calling the + * appropriate handler based on the message command. + * + * @param message - The FIDO2 extension message to handle. + * @param sender - The sender of the message. + * @param sendResponse - The function to call with the response. + */ + private handleExtensionMessage = ( + message: Fido2ExtensionMessage, + sender: chrome.runtime.MessageSender, + sendResponse: (response?: any) => void, + ) => { + const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command]; + if (!handler) { + return; + } + + const messageResponse = handler({ message, sender }); + if (!messageResponse) { + return; + } + + Promise.resolve(messageResponse) + .then( + (response) => sendResponse(response), + (error) => sendResponse({ error: { ...error, message: error.message } }), + ) + .catch(this.logService.error); + + return true; + }; + + /** + * Handles the connection of a FIDO2 content script port by checking if the + * FIDO2 feature is enabled for the sender's hostname and origin. If the feature + * is not enabled, the port is disconnected. + * + * @param port - The port which is connecting + */ + private handleInjectedScriptPortConnection = async (port: chrome.runtime.Port) => { + if (port.name !== Fido2PortName.InjectedScript || !port.sender?.url) { + return; + } + + try { + const { hostname, origin } = new URL(port.sender.url); + if (!(await this.fido2ClientService.isFido2FeatureEnabled(hostname, origin))) { + port.disconnect(); + return; + } + + this.fido2ContentScriptPortsSet.add(port); + port.onDisconnect.addListener(this.handleInjectScriptPortOnDisconnect); + } catch (error) { + this.logService.error(error); + port.disconnect(); + } + }; + + /** + * Handles the disconnection of a FIDO2 content script port + * by removing it from the set of connected ports. + * + * @param port - The port which is disconnecting + */ + private handleInjectScriptPortOnDisconnect = (port: chrome.runtime.Port) => { + if (port.name !== Fido2PortName.InjectedScript) { + return; + } + + this.fido2ContentScriptPortsSet.delete(port); + }; +} diff --git a/apps/browser/src/vault/fido2/content/content-script.spec.ts b/apps/browser/src/vault/fido2/content/content-script.spec.ts new file mode 100644 index 0000000000..29d3e9c257 --- /dev/null +++ b/apps/browser/src/vault/fido2/content/content-script.spec.ts @@ -0,0 +1,164 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { CreateCredentialResult } from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction"; + +import { createPortSpyMock } from "../../../autofill/spec/autofill-mocks"; +import { triggerPortOnDisconnectEvent } from "../../../autofill/spec/testing-utils"; +import { Fido2PortName } from "../enums/fido2-port-name.enum"; + +import { InsecureCreateCredentialParams, MessageType } from "./messaging/message"; +import { MessageWithMetadata, Messenger } from "./messaging/messenger"; + +jest.mock("../../../autofill/utils", () => ({ + sendExtensionMessage: jest.fn((command, options) => { + return chrome.runtime.sendMessage(Object.assign({ command }, options)); + }), +})); + +describe("Fido2 Content Script", () => { + let messenger: Messenger; + const messengerForDOMCommunicationSpy = jest + .spyOn(Messenger, "forDOMCommunication") + .mockImplementation((window) => { + const windowOrigin = window.location.origin; + + messenger = new Messenger({ + postMessage: (message, port) => window.postMessage(message, windowOrigin, [port]), + addEventListener: (listener) => window.addEventListener("message", listener), + removeEventListener: (listener) => window.removeEventListener("message", listener), + }); + messenger.destroy = jest.fn(); + return messenger; + }); + const portSpy: MockProxy = createPortSpyMock(Fido2PortName.InjectedScript); + chrome.runtime.connect = jest.fn(() => portSpy); + + afterEach(() => { + Object.defineProperty(document, "contentType", { + value: "text/html", + writable: true, + }); + + jest.clearAllMocks(); + jest.resetModules(); + }); + + it("destroys the messenger when the port is disconnected", () => { + require("./content-script"); + + triggerPortOnDisconnectEvent(portSpy); + + expect(messenger.destroy).toHaveBeenCalled(); + }); + + it("handles a FIDO2 credential creation request message from the window message listener, formats the message and sends the formatted message to the extension background", async () => { + const message = mock({ + type: MessageType.CredentialCreationRequest, + data: mock(), + }); + const mockResult = { credentialId: "mock" } as CreateCredentialResult; + jest.spyOn(chrome.runtime, "sendMessage").mockResolvedValue(mockResult); + + require("./content-script"); + + const response = await messenger.handler!(message, new AbortController()); + + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + command: "fido2RegisterCredentialRequest", + data: expect.objectContaining({ + origin: globalThis.location.origin, + sameOriginWithAncestors: true, + }), + requestId: expect.any(String), + }); + expect(response).toEqual({ + type: MessageType.CredentialCreationResponse, + result: mockResult, + }); + }); + + it("handles a FIDO2 credential get request message from the window message listener, formats the message and sends the formatted message to the extension background", async () => { + const message = mock({ + type: MessageType.CredentialGetRequest, + data: mock(), + }); + + require("./content-script"); + + await messenger.handler!(message, new AbortController()); + + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + command: "fido2GetCredentialRequest", + data: expect.objectContaining({ + origin: globalThis.location.origin, + sameOriginWithAncestors: true, + }), + requestId: expect.any(String), + }); + }); + + it("removes the abort handler when the FIDO2 request is complete", async () => { + const message = mock({ + type: MessageType.CredentialCreationRequest, + data: mock(), + }); + const abortController = new AbortController(); + const abortSpy = jest.spyOn(abortController.signal, "removeEventListener"); + + require("./content-script"); + + await messenger.handler!(message, abortController); + + expect(abortSpy).toHaveBeenCalled(); + }); + + it("sends an extension message to abort the FIDO2 request when the abort controller is signaled", async () => { + const message = mock({ + type: MessageType.CredentialCreationRequest, + data: mock(), + }); + const abortController = new AbortController(); + const abortSpy = jest.spyOn(abortController.signal, "addEventListener"); + jest + .spyOn(chrome.runtime, "sendMessage") + .mockImplementationOnce(async (extensionId: string, message: unknown, options: any) => { + abortController.abort(); + }); + + require("./content-script"); + + await messenger.handler!(message, abortController); + + expect(abortSpy).toHaveBeenCalled(); + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + command: "fido2AbortRequest", + abortedRequestId: expect.any(String), + }); + }); + + it("rejects credential requests and returns an error result", async () => { + const errorMessage = "Test error"; + const message = mock({ + type: MessageType.CredentialCreationRequest, + data: mock(), + }); + const abortController = new AbortController(); + jest.spyOn(chrome.runtime, "sendMessage").mockResolvedValue({ error: errorMessage }); + + require("./content-script"); + const result = messenger.handler!(message, abortController); + + await expect(result).rejects.toEqual(errorMessage); + }); + + it("skips initializing the content script if the document content type is not 'text/html'", () => { + Object.defineProperty(document, "contentType", { + value: "application/json", + writable: true, + }); + + require("./content-script"); + + expect(messengerForDOMCommunicationSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/browser/src/vault/fido2/content/content-script.ts b/apps/browser/src/vault/fido2/content/content-script.ts index c2fc862f55..809db11553 100644 --- a/apps/browser/src/vault/fido2/content/content-script.ts +++ b/apps/browser/src/vault/fido2/content/content-script.ts @@ -3,142 +3,134 @@ import { CreateCredentialParams, } from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction"; -import { Message, MessageType } from "./messaging/message"; -import { Messenger } from "./messaging/messenger"; +import { sendExtensionMessage } from "../../../autofill/utils"; +import { Fido2PortName } from "../enums/fido2-port-name.enum"; -function isFido2FeatureEnabled(): Promise { - return new Promise((resolve) => { - chrome.runtime.sendMessage( - { - command: "checkFido2FeatureEnabled", - hostname: window.location.hostname, - origin: window.location.origin, - }, - (response: { result?: boolean }) => resolve(response.result), - ); - }); -} - -function isSameOriginWithAncestors() { - try { - return window.self === window.top; - } catch { - return false; - } -} -const messenger = Messenger.forDOMCommunication(window); - -function injectPageScript() { - // Locate an existing page-script on the page - const existingPageScript = document.getElementById("bw-fido2-page-script"); - - // Inject the page-script if it doesn't exist - if (!existingPageScript) { - const s = document.createElement("script"); - s.src = chrome.runtime.getURL("content/fido2/page-script.js"); - s.id = "bw-fido2-page-script"; - (document.head || document.documentElement).appendChild(s); +import { + InsecureAssertCredentialParams, + InsecureCreateCredentialParams, + Message, + MessageType, +} from "./messaging/message"; +import { MessageWithMetadata, Messenger } from "./messaging/messenger"; +(function (globalContext) { + if (globalContext.document.contentType !== "text/html") { return; } - // If the page-script already exists, send a reconnect message to the page-script - // 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 - messenger.sendReconnectCommand(); -} + // Initialization logic, set up the messenger and connect a port to the background script. + const messenger = Messenger.forDOMCommunication(globalContext.window); + messenger.handler = handleFido2Message; + const port = chrome.runtime.connect({ name: Fido2PortName.InjectedScript }); + port.onDisconnect.addListener(handlePortOnDisconnect); -function initializeFido2ContentScript() { - injectPageScript(); - - messenger.handler = async (message, abortController) => { + /** + * Handles FIDO2 credential requests and returns the result. + * + * @param message - The message to handle. + * @param abortController - The abort controller used to handle exit conditions from the FIDO2 request. + */ + async function handleFido2Message( + message: MessageWithMetadata, + abortController: AbortController, + ) { const requestId = Date.now().toString(); const abortHandler = () => - chrome.runtime.sendMessage({ - command: "fido2AbortRequest", - abortedRequestId: requestId, - }); + sendExtensionMessage("fido2AbortRequest", { abortedRequestId: requestId }); abortController.signal.addEventListener("abort", abortHandler); - if (message.type === MessageType.CredentialCreationRequest) { - return new Promise((resolve, reject) => { - const data: CreateCredentialParams = { - ...message.data, - origin: window.location.origin, - sameOriginWithAncestors: isSameOriginWithAncestors(), - }; - - chrome.runtime.sendMessage( - { - command: "fido2RegisterCredentialRequest", - data, - requestId: requestId, - }, - (response) => { - if (response && response.error !== undefined) { - return reject(response.error); - } - - resolve({ - type: MessageType.CredentialCreationResponse, - result: response.result, - }); - }, + try { + if (message.type === MessageType.CredentialCreationRequest) { + return handleCredentialCreationRequestMessage( + requestId, + message.data as InsecureCreateCredentialParams, ); - }); - } + } - if (message.type === MessageType.CredentialGetRequest) { - return new Promise((resolve, reject) => { - const data: AssertCredentialParams = { - ...message.data, - origin: window.location.origin, - sameOriginWithAncestors: isSameOriginWithAncestors(), - }; - - chrome.runtime.sendMessage( - { - command: "fido2GetCredentialRequest", - data, - requestId: requestId, - }, - (response) => { - if (response && response.error !== undefined) { - return reject(response.error); - } - - resolve({ - type: MessageType.CredentialGetResponse, - result: response.result, - }); - }, + if (message.type === MessageType.CredentialGetRequest) { + return handleCredentialGetRequestMessage( + requestId, + message.data as InsecureAssertCredentialParams, ); - }).finally(() => - abortController.signal.removeEventListener("abort", abortHandler), - ) as Promise; + } + } finally { + abortController.signal.removeEventListener("abort", abortHandler); } - - return undefined; - }; -} - -async function run() { - if (!(await isFido2FeatureEnabled())) { - return; } - initializeFido2ContentScript(); + /** + * Handles the credential creation request message and returns the result. + * + * @param requestId - The request ID of the message. + * @param data - Data associated with the credential request. + */ + async function handleCredentialCreationRequestMessage( + requestId: string, + data: InsecureCreateCredentialParams, + ): Promise { + return respondToCredentialRequest( + "fido2RegisterCredentialRequest", + MessageType.CredentialCreationResponse, + requestId, + data, + ); + } - const port = chrome.runtime.connect({ name: "fido2ContentScriptReady" }); - port.onDisconnect.addListener(() => { - // Cleanup the messenger and remove the event listener - // 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 - messenger.destroy(); - }); -} + /** + * Handles the credential get request message and returns the result. + * + * @param requestId - The request ID of the message. + * @param data - Data associated with the credential request. + */ + async function handleCredentialGetRequestMessage( + requestId: string, + data: InsecureAssertCredentialParams, + ): Promise { + return respondToCredentialRequest( + "fido2GetCredentialRequest", + MessageType.CredentialGetResponse, + requestId, + data, + ); + } -// Only run the script if the document is an HTML document -if (document.contentType === "text/html") { - void run(); -} + /** + * Sends a message to the extension to handle the + * credential request and returns the result. + * + * @param command - The command to send to the extension. + * @param type - The type of message, either CredentialCreationResponse or CredentialGetResponse. + * @param requestId - The request ID of the message. + * @param messageData - Data associated with the credential request. + */ + async function respondToCredentialRequest( + command: string, + type: MessageType.CredentialCreationResponse | MessageType.CredentialGetResponse, + requestId: string, + messageData: InsecureCreateCredentialParams | InsecureAssertCredentialParams, + ): Promise { + const data: CreateCredentialParams | AssertCredentialParams = { + ...messageData, + origin: globalContext.location.origin, + sameOriginWithAncestors: globalContext.self === globalContext.top, + }; + + const result = await sendExtensionMessage(command, { data, requestId }); + + if (result && result.error !== undefined) { + return Promise.reject(result.error); + } + + return Promise.resolve({ type, result }); + } + + /** + * Handles the disconnect event of the port. Calls + * to the messenger to destroy and tear down the + * implemented page-script.js logic. + */ + function handlePortOnDisconnect() { + void messenger.destroy(); + } +})(globalThis); diff --git a/apps/browser/src/vault/fido2/content/messaging/messenger.spec.ts b/apps/browser/src/vault/fido2/content/messaging/messenger.spec.ts index 0c46ac39aa..5283c60882 100644 --- a/apps/browser/src/vault/fido2/content/messaging/messenger.spec.ts +++ b/apps/browser/src/vault/fido2/content/messaging/messenger.spec.ts @@ -68,7 +68,7 @@ describe("Messenger", () => { const abortController = new AbortController(); // 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 - messengerA.request(createRequest(), abortController); + messengerA.request(createRequest(), abortController.signal); abortController.abort(); const received = handlerB.receive(); diff --git a/apps/browser/src/vault/fido2/content/messaging/messenger.ts b/apps/browser/src/vault/fido2/content/messaging/messenger.ts index cc29282227..f05c138eab 100644 --- a/apps/browser/src/vault/fido2/content/messaging/messenger.ts +++ b/apps/browser/src/vault/fido2/content/messaging/messenger.ts @@ -47,7 +47,7 @@ export class Messenger { } /** - * The handler that will be called when a message is recieved. The handler should return + * The handler that will be called when a message is received. The handler should return * a promise that resolves to the response message. If the handler throws an error, the * error will be sent back to the sender. */ @@ -65,10 +65,10 @@ export class Messenger { * AbortController signals will be forwarded to the content script. * * @param request data to send to the content script - * @param abortController the abort controller that might be used to abort the request + * @param abortSignal the abort controller that might be used to abort the request * @returns the response from the content script */ - async request(request: Message, abortController?: AbortController): Promise { + async request(request: Message, abortSignal?: AbortSignal): Promise { const requestChannel = new MessageChannel(); const { port1: localPort, port2: remotePort } = requestChannel; @@ -82,7 +82,7 @@ export class Messenger { metadata: { SENDER }, type: MessageType.AbortRequest, }); - abortController?.signal.addEventListener("abort", abortListener); + abortSignal?.addEventListener("abort", abortListener); this.broadcastChannel.postMessage( { ...request, SENDER, senderId: this.messengerId }, @@ -90,7 +90,7 @@ export class Messenger { ); const response = await promise; - abortController?.signal.removeEventListener("abort", abortListener); + abortSignal?.removeEventListener("abort", abortListener); if (response.type === MessageType.ErrorResponse) { const error = new Error(); @@ -113,12 +113,7 @@ export class Messenger { const message = event.data; const port = event.ports?.[0]; - if ( - message?.SENDER !== SENDER || - message.senderId == this.messengerId || - message == null || - port == null - ) { + if (message?.SENDER !== SENDER || message.senderId == this.messengerId || port == null) { return; } @@ -167,10 +162,6 @@ export class Messenger { } } - async sendReconnectCommand() { - await this.request({ type: MessageType.ReconnectRequest }); - } - private async sendDisconnectCommand() { await this.request({ type: MessageType.DisconnectRequest }); } diff --git a/apps/browser/src/vault/fido2/content/page-script-append.mv2.spec.ts b/apps/browser/src/vault/fido2/content/page-script-append.mv2.spec.ts new file mode 100644 index 0000000000..d40a725a1f --- /dev/null +++ b/apps/browser/src/vault/fido2/content/page-script-append.mv2.spec.ts @@ -0,0 +1,69 @@ +import { Fido2ContentScript } from "../enums/fido2-content-script.enum"; + +describe("FIDO2 page-script for manifest v2", () => { + let createdScriptElement: HTMLScriptElement; + jest.spyOn(window.document, "createElement"); + + afterEach(() => { + Object.defineProperty(window.document, "contentType", { value: "text/html", writable: true }); + jest.clearAllMocks(); + jest.resetModules(); + }); + + it("skips appending the `page-script.js` file if the document contentType is not `text/html`", () => { + Object.defineProperty(window.document, "contentType", { value: "text/plain", writable: true }); + + require("./page-script-append.mv2"); + + expect(window.document.createElement).not.toHaveBeenCalled(); + }); + + it("appends the `page-script.js` file to the document head when the contentType is `text/html`", () => { + jest.spyOn(window.document.head, "insertBefore").mockImplementation((node) => { + createdScriptElement = node as HTMLScriptElement; + return node; + }); + + require("./page-script-append.mv2"); + + expect(window.document.createElement).toHaveBeenCalledWith("script"); + expect(chrome.runtime.getURL).toHaveBeenCalledWith(Fido2ContentScript.PageScript); + expect(window.document.head.insertBefore).toHaveBeenCalledWith( + expect.any(HTMLScriptElement), + window.document.head.firstChild, + ); + expect(createdScriptElement.src).toBe(`chrome-extension://id/${Fido2ContentScript.PageScript}`); + }); + + it("appends the `page-script.js` file to the document element if the head is not available", () => { + window.document.documentElement.removeChild(window.document.head); + jest.spyOn(window.document.documentElement, "insertBefore").mockImplementation((node) => { + createdScriptElement = node as HTMLScriptElement; + return node; + }); + + require("./page-script-append.mv2"); + + expect(window.document.createElement).toHaveBeenCalledWith("script"); + expect(chrome.runtime.getURL).toHaveBeenCalledWith(Fido2ContentScript.PageScript); + expect(window.document.documentElement.insertBefore).toHaveBeenCalledWith( + expect.any(HTMLScriptElement), + window.document.documentElement.firstChild, + ); + expect(createdScriptElement.src).toBe(`chrome-extension://id/${Fido2ContentScript.PageScript}`); + }); + + it("removes the appended `page-script.js` file after the script has triggered a load event", () => { + createdScriptElement = document.createElement("script"); + jest.spyOn(window.document, "createElement").mockImplementation((element) => { + return createdScriptElement; + }); + + require("./page-script-append.mv2"); + + jest.spyOn(createdScriptElement, "remove"); + createdScriptElement.dispatchEvent(new Event("load")); + + expect(createdScriptElement.remove).toHaveBeenCalled(); + }); +}); diff --git a/apps/browser/src/vault/fido2/content/page-script-append.mv2.ts b/apps/browser/src/vault/fido2/content/page-script-append.mv2.ts new file mode 100644 index 0000000000..4e806d2990 --- /dev/null +++ b/apps/browser/src/vault/fido2/content/page-script-append.mv2.ts @@ -0,0 +1,19 @@ +/** + * This script handles injection of the FIDO2 override page script into the document. + * This is required for manifest v2, but will be removed when we migrate fully to manifest v3. + */ +import { Fido2ContentScript } from "../enums/fido2-content-script.enum"; + +(function (globalContext) { + if (globalContext.document.contentType !== "text/html") { + return; + } + + const script = globalContext.document.createElement("script"); + script.src = chrome.runtime.getURL(Fido2ContentScript.PageScript); + script.addEventListener("load", () => script.remove()); + + const scriptInsertionPoint = + globalContext.document.head || globalContext.document.documentElement; + scriptInsertionPoint.insertBefore(script, scriptInsertionPoint.firstChild); +})(globalThis); diff --git a/apps/browser/src/vault/fido2/content/page-script.ts b/apps/browser/src/vault/fido2/content/page-script.ts index 9adea68307..1de0f3258a 100644 --- a/apps/browser/src/vault/fido2/content/page-script.ts +++ b/apps/browser/src/vault/fido2/content/page-script.ts @@ -5,212 +5,229 @@ import { WebauthnUtils } from "../webauthn-utils"; import { MessageType } from "./messaging/message"; import { Messenger } from "./messaging/messenger"; -const BrowserPublicKeyCredential = window.PublicKeyCredential; - -const browserNativeWebauthnSupport = window.PublicKeyCredential != undefined; -let browserNativeWebauthnPlatformAuthenticatorSupport = false; -if (!browserNativeWebauthnSupport) { - // Polyfill webauthn support - try { - // credentials is read-only if supported, use type-casting to force assignment - (navigator as any).credentials = { - async create() { - throw new Error("Webauthn not supported in this browser."); - }, - async get() { - throw new Error("Webauthn not supported in this browser."); - }, - }; - window.PublicKeyCredential = class PolyfillPublicKeyCredential { - static isUserVerifyingPlatformAuthenticatorAvailable() { - return Promise.resolve(true); - } - } as any; - window.AuthenticatorAttestationResponse = - class PolyfillAuthenticatorAttestationResponse {} as any; - } catch { - /* empty */ +(function (globalContext) { + if (globalContext.document.contentType !== "text/html") { + return; } -} + const BrowserPublicKeyCredential = globalContext.PublicKeyCredential; + const BrowserNavigatorCredentials = navigator.credentials; + const BrowserAuthenticatorAttestationResponse = globalContext.AuthenticatorAttestationResponse; -if (browserNativeWebauthnSupport) { - // 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 - BrowserPublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable().then((available) => { - browserNativeWebauthnPlatformAuthenticatorSupport = available; - - if (!available) { - // Polyfill platform authenticator support - window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable = () => - Promise.resolve(true); + const browserNativeWebauthnSupport = globalContext.PublicKeyCredential != undefined; + let browserNativeWebauthnPlatformAuthenticatorSupport = false; + if (!browserNativeWebauthnSupport) { + // Polyfill webauthn support + try { + // credentials are read-only if supported, use type-casting to force assignment + (navigator as any).credentials = { + async create() { + throw new Error("Webauthn not supported in this browser."); + }, + async get() { + throw new Error("Webauthn not supported in this browser."); + }, + }; + globalContext.PublicKeyCredential = class PolyfillPublicKeyCredential { + static isUserVerifyingPlatformAuthenticatorAvailable() { + return Promise.resolve(true); + } + } as any; + globalContext.AuthenticatorAttestationResponse = + class PolyfillAuthenticatorAttestationResponse {} as any; + } catch { + /* empty */ } - }); -} + } else { + void BrowserPublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable().then( + (available) => { + browserNativeWebauthnPlatformAuthenticatorSupport = available; -const browserCredentials = { - create: navigator.credentials.create.bind( - navigator.credentials, - ) as typeof navigator.credentials.create, - get: navigator.credentials.get.bind(navigator.credentials) as typeof navigator.credentials.get, -}; - -const messenger = ((window as any).messenger = Messenger.forDOMCommunication(window)); - -navigator.credentials.create = createWebAuthnCredential; -navigator.credentials.get = getWebAuthnCredential; - -/** - * Creates a new webauthn credential. - * - * @param options Options for creating new credentials. - * @param abortController Abort controller to abort the request if needed. - * @returns Promise that resolves to the new credential object. - */ -async function createWebAuthnCredential( - options?: CredentialCreationOptions, - abortController?: AbortController, -): Promise { - if (!isWebauthnCall(options)) { - return await browserCredentials.create(options); - } - - const fallbackSupported = - (options?.publicKey?.authenticatorSelection?.authenticatorAttachment === "platform" && - browserNativeWebauthnPlatformAuthenticatorSupport) || - (options?.publicKey?.authenticatorSelection?.authenticatorAttachment !== "platform" && - browserNativeWebauthnSupport); - try { - const response = await messenger.request( - { - type: MessageType.CredentialCreationRequest, - data: WebauthnUtils.mapCredentialCreationOptions(options, fallbackSupported), + if (!available) { + // Polyfill platform authenticator support + globalContext.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable = () => + Promise.resolve(true); + } }, - abortController, ); + } - if (response.type !== MessageType.CredentialCreationResponse) { - throw new Error("Something went wrong."); - } + const browserCredentials = { + create: navigator.credentials.create.bind( + navigator.credentials, + ) as typeof navigator.credentials.create, + get: navigator.credentials.get.bind(navigator.credentials) as typeof navigator.credentials.get, + }; - return WebauthnUtils.mapCredentialRegistrationResult(response.result); - } catch (error) { - if (error && error.fallbackRequested && fallbackSupported) { - await waitForFocus(); + const messenger = Messenger.forDOMCommunication(window); + let waitForFocusTimeout: number | NodeJS.Timeout; + let focusListenerHandler: () => void; + + navigator.credentials.create = createWebAuthnCredential; + navigator.credentials.get = getWebAuthnCredential; + + /** + * Creates a new webauthn credential. + * + * @param options Options for creating new credentials. + * @returns Promise that resolves to the new credential object. + */ + async function createWebAuthnCredential( + options?: CredentialCreationOptions, + ): Promise { + if (!isWebauthnCall(options)) { return await browserCredentials.create(options); } - throw error; - } -} + const authenticatorAttachmentIsPlatform = + options?.publicKey?.authenticatorSelection?.authenticatorAttachment === "platform"; -/** - * Retrieves a webauthn credential. - * - * @param options Options for creating new credentials. - * @param abortController Abort controller to abort the request if needed. - * @returns Promise that resolves to the new credential object. - */ -async function getWebAuthnCredential( - options?: CredentialRequestOptions, - abortController?: AbortController, -): Promise { - if (!isWebauthnCall(options)) { - return await browserCredentials.get(options); + const fallbackSupported = + (authenticatorAttachmentIsPlatform && browserNativeWebauthnPlatformAuthenticatorSupport) || + (!authenticatorAttachmentIsPlatform && browserNativeWebauthnSupport); + try { + const response = await messenger.request( + { + type: MessageType.CredentialCreationRequest, + data: WebauthnUtils.mapCredentialCreationOptions(options, fallbackSupported), + }, + options?.signal, + ); + + if (response.type !== MessageType.CredentialCreationResponse) { + throw new Error("Something went wrong."); + } + + return WebauthnUtils.mapCredentialRegistrationResult(response.result); + } catch (error) { + if (error && error.fallbackRequested && fallbackSupported) { + await waitForFocus(); + return await browserCredentials.create(options); + } + + throw error; + } } - const fallbackSupported = browserNativeWebauthnSupport; - - try { - if (options?.mediation && options.mediation !== "optional") { - throw new FallbackRequestedError(); - } - - const response = await messenger.request( - { - type: MessageType.CredentialGetRequest, - data: WebauthnUtils.mapCredentialRequestOptions(options, fallbackSupported), - }, - abortController, - ); - - if (response.type !== MessageType.CredentialGetResponse) { - throw new Error("Something went wrong."); - } - - return WebauthnUtils.mapCredentialAssertResult(response.result); - } catch (error) { - if (error && error.fallbackRequested && fallbackSupported) { - await waitForFocus(); + /** + * Retrieves a webauthn credential. + * + * @param options Options for creating new credentials. + * @returns Promise that resolves to the new credential object. + */ + async function getWebAuthnCredential(options?: CredentialRequestOptions): Promise { + if (!isWebauthnCall(options)) { return await browserCredentials.get(options); } - throw error; - } -} + const fallbackSupported = browserNativeWebauthnSupport; -function isWebauthnCall(options?: CredentialCreationOptions | CredentialRequestOptions) { - return options && "publicKey" in options; -} + try { + if (options?.mediation && options.mediation !== "optional") { + throw new FallbackRequestedError(); + } -/** - * Wait for window to be focused. - * Safari doesn't allow scripts to trigger webauthn when window is not focused. - * - * @param fallbackWait How long to wait when the script is not able to add event listeners to `window.top`. Defaults to 500ms. - * @param timeout Maximum time to wait for focus in milliseconds. Defaults to 5 minutes. - * @returns Promise that resolves when window is focused, or rejects if timeout is reached. - */ -async function waitForFocus(fallbackWait = 500, timeout = 5 * 60 * 1000) { - try { - if (window.top.document.hasFocus()) { - return; + const response = await messenger.request( + { + type: MessageType.CredentialGetRequest, + data: WebauthnUtils.mapCredentialRequestOptions(options, fallbackSupported), + }, + options?.signal, + ); + + if (response.type !== MessageType.CredentialGetResponse) { + throw new Error("Something went wrong."); + } + + return WebauthnUtils.mapCredentialAssertResult(response.result); + } catch (error) { + if (error && error.fallbackRequested && fallbackSupported) { + await waitForFocus(); + return await browserCredentials.get(options); + } + + throw error; } - } catch { - // Cannot access window.top due to cross-origin frame, fallback to waiting - return await new Promise((resolve) => window.setTimeout(resolve, fallbackWait)); } - let focusListener; - const focusPromise = new Promise((resolve) => { - focusListener = () => resolve(); - window.top.addEventListener("focus", focusListener); - }); - - let timeoutId; - const timeoutPromise = new Promise((_, reject) => { - timeoutId = window.setTimeout( - () => - reject( - new DOMException("The operation either timed out or was not allowed.", "AbortError"), - ), - timeout, - ); - }); - - try { - await Promise.race([focusPromise, timeoutPromise]); - } finally { - window.top.removeEventListener("focus", focusListener); - window.clearTimeout(timeoutId); - } -} - -/** - * Sets up a listener to handle cleanup or reconnection when the extension's - * context changes due to being reloaded or unloaded. - */ -messenger.handler = (message, abortController) => { - const type = message.type; - - // Handle cleanup for disconnect request - if (type === MessageType.DisconnectRequest && browserNativeWebauthnSupport) { - navigator.credentials.create = browserCredentials.create; - navigator.credentials.get = browserCredentials.get; + function isWebauthnCall(options?: CredentialCreationOptions | CredentialRequestOptions) { + return options && "publicKey" in options; } - // Handle reinitialization for reconnect request - if (type === MessageType.ReconnectRequest && browserNativeWebauthnSupport) { - navigator.credentials.create = createWebAuthnCredential; - navigator.credentials.get = getWebAuthnCredential; + /** + * Wait for window to be focused. + * Safari doesn't allow scripts to trigger webauthn when window is not focused. + * + * @param fallbackWait How long to wait when the script is not able to add event listeners to `window.top`. Defaults to 500ms. + * @param timeout Maximum time to wait for focus in milliseconds. Defaults to 5 minutes. + * @returns Promise that resolves when window is focused, or rejects if timeout is reached. + */ + async function waitForFocus(fallbackWait = 500, timeout = 5 * 60 * 1000) { + try { + if (globalContext.top.document.hasFocus()) { + return; + } + } catch { + // Cannot access window.top due to cross-origin frame, fallback to waiting + return await new Promise((resolve) => globalContext.setTimeout(resolve, fallbackWait)); + } + + const focusPromise = new Promise((resolve) => { + focusListenerHandler = () => resolve(); + globalContext.top.addEventListener("focus", focusListenerHandler); + }); + + const timeoutPromise = new Promise((_, reject) => { + waitForFocusTimeout = globalContext.setTimeout( + () => + reject( + new DOMException("The operation either timed out or was not allowed.", "AbortError"), + ), + timeout, + ); + }); + + try { + await Promise.race([focusPromise, timeoutPromise]); + } finally { + clearWaitForFocus(); + } } -}; + + function clearWaitForFocus() { + globalContext.top.removeEventListener("focus", focusListenerHandler); + if (waitForFocusTimeout) { + globalContext.clearTimeout(waitForFocusTimeout); + } + } + + function destroy() { + try { + if (browserNativeWebauthnSupport) { + navigator.credentials.create = browserCredentials.create; + navigator.credentials.get = browserCredentials.get; + } else { + (navigator as any).credentials = BrowserNavigatorCredentials; + globalContext.PublicKeyCredential = BrowserPublicKeyCredential; + globalContext.AuthenticatorAttestationResponse = BrowserAuthenticatorAttestationResponse; + } + + clearWaitForFocus(); + void messenger.destroy(); + } catch (e) { + /** empty */ + } + } + + /** + * Sets up a listener to handle cleanup or reconnection when the extension's + * context changes due to being reloaded or unloaded. + */ + messenger.handler = (message) => { + const type = message.type; + + // Handle cleanup for disconnect request + if (type === MessageType.DisconnectRequest) { + destroy(); + } + }; +})(globalThis); diff --git a/apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts b/apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts new file mode 100644 index 0000000000..211959c466 --- /dev/null +++ b/apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts @@ -0,0 +1,121 @@ +import { + createAssertCredentialResultMock, + createCreateCredentialResultMock, + createCredentialCreationOptionsMock, + createCredentialRequestOptionsMock, + setupMockedWebAuthnSupport, +} from "../../../autofill/spec/fido2-testing-utils"; +import { WebauthnUtils } from "../webauthn-utils"; + +import { MessageType } from "./messaging/message"; +import { Messenger } from "./messaging/messenger"; + +let messenger: Messenger; +jest.mock("./messaging/messenger", () => { + return { + Messenger: class extends jest.requireActual("./messaging/messenger").Messenger { + static forDOMCommunication: any = jest.fn((window) => { + const windowOrigin = window.location.origin; + + messenger = new Messenger({ + postMessage: (message, port) => window.postMessage(message, windowOrigin, [port]), + addEventListener: (listener) => window.addEventListener("message", listener), + removeEventListener: (listener) => window.removeEventListener("message", listener), + }); + messenger.destroy = jest.fn(); + return messenger; + }); + }, + }; +}); +jest.mock("../webauthn-utils"); + +describe("Fido2 page script with native WebAuthn support", () => { + const mockCredentialCreationOptions = createCredentialCreationOptionsMock(); + const mockCreateCredentialsResult = createCreateCredentialResultMock(); + const mockCredentialRequestOptions = createCredentialRequestOptionsMock(); + const mockCredentialAssertResult = createAssertCredentialResultMock(); + setupMockedWebAuthnSupport(); + + require("./page-script"); + + afterAll(() => { + jest.clearAllMocks(); + jest.resetModules(); + }); + + describe("creating WebAuthn credentials", () => { + beforeEach(() => { + messenger.request = jest.fn().mockResolvedValue({ + type: MessageType.CredentialCreationResponse, + result: mockCreateCredentialsResult, + }); + }); + + it("falls back to the default browser credentials API if an error occurs", async () => { + window.top.document.hasFocus = jest.fn().mockReturnValue(true); + messenger.request = jest.fn().mockRejectedValue({ fallbackRequested: true }); + + try { + await navigator.credentials.create(mockCredentialCreationOptions); + expect("This will fail the test").toBe(true); + } catch { + expect(WebauthnUtils.mapCredentialRegistrationResult).not.toHaveBeenCalled(); + } + }); + + it("creates and returns a WebAuthn credential when the navigator API is called to create credentials", async () => { + await navigator.credentials.create(mockCredentialCreationOptions); + + expect(WebauthnUtils.mapCredentialCreationOptions).toHaveBeenCalledWith( + mockCredentialCreationOptions, + true, + ); + expect(WebauthnUtils.mapCredentialRegistrationResult).toHaveBeenCalledWith( + mockCreateCredentialsResult, + ); + }); + }); + + describe("get WebAuthn credentials", () => { + beforeEach(() => { + messenger.request = jest.fn().mockResolvedValue({ + type: MessageType.CredentialGetResponse, + result: mockCredentialAssertResult, + }); + }); + + it("falls back to the default browser credentials API when an error occurs", async () => { + window.top.document.hasFocus = jest.fn().mockReturnValue(true); + messenger.request = jest.fn().mockRejectedValue({ fallbackRequested: true }); + + const returnValue = await navigator.credentials.get(mockCredentialRequestOptions); + + expect(returnValue).toBeDefined(); + expect(WebauthnUtils.mapCredentialAssertResult).not.toHaveBeenCalled(); + }); + + it("gets and returns the WebAuthn credentials", async () => { + await navigator.credentials.get(mockCredentialRequestOptions); + + expect(WebauthnUtils.mapCredentialRequestOptions).toHaveBeenCalledWith( + mockCredentialRequestOptions, + true, + ); + expect(WebauthnUtils.mapCredentialAssertResult).toHaveBeenCalledWith( + mockCredentialAssertResult, + ); + }); + }); + + describe("destroy", () => { + it("should destroy the message listener when receiving a disconnect request", async () => { + jest.spyOn(globalThis.top, "removeEventListener"); + const SENDER = "bitwarden-webauthn"; + void messenger.handler({ type: MessageType.DisconnectRequest, SENDER, senderId: "1" }); + + expect(globalThis.top.removeEventListener).toHaveBeenCalledWith("focus", undefined); + expect(messenger.destroy).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/vault/fido2/content/page-script.webauthn-unsupported.spec.ts b/apps/browser/src/vault/fido2/content/page-script.webauthn-unsupported.spec.ts new file mode 100644 index 0000000000..f3aee685e1 --- /dev/null +++ b/apps/browser/src/vault/fido2/content/page-script.webauthn-unsupported.spec.ts @@ -0,0 +1,96 @@ +import { + createAssertCredentialResultMock, + createCreateCredentialResultMock, + createCredentialCreationOptionsMock, + createCredentialRequestOptionsMock, +} from "../../../autofill/spec/fido2-testing-utils"; +import { WebauthnUtils } from "../webauthn-utils"; + +import { MessageType } from "./messaging/message"; +import { Messenger } from "./messaging/messenger"; + +let messenger: Messenger; +jest.mock("./messaging/messenger", () => { + return { + Messenger: class extends jest.requireActual("./messaging/messenger").Messenger { + static forDOMCommunication: any = jest.fn((window) => { + const windowOrigin = window.location.origin; + + messenger = new Messenger({ + postMessage: (message, port) => window.postMessage(message, windowOrigin, [port]), + addEventListener: (listener) => window.addEventListener("message", listener), + removeEventListener: (listener) => window.removeEventListener("message", listener), + }); + messenger.destroy = jest.fn(); + return messenger; + }); + }, + }; +}); +jest.mock("../webauthn-utils"); + +describe("Fido2 page script without native WebAuthn support", () => { + const mockCredentialCreationOptions = createCredentialCreationOptionsMock(); + const mockCreateCredentialsResult = createCreateCredentialResultMock(); + const mockCredentialRequestOptions = createCredentialRequestOptionsMock(); + const mockCredentialAssertResult = createAssertCredentialResultMock(); + require("./page-script"); + + afterAll(() => { + jest.clearAllMocks(); + jest.resetModules(); + }); + + describe("creating WebAuthn credentials", () => { + beforeEach(() => { + messenger.request = jest.fn().mockResolvedValue({ + type: MessageType.CredentialCreationResponse, + result: mockCreateCredentialsResult, + }); + }); + + it("creates and returns a WebAuthn credential", async () => { + await navigator.credentials.create(mockCredentialCreationOptions); + + expect(WebauthnUtils.mapCredentialCreationOptions).toHaveBeenCalledWith( + mockCredentialCreationOptions, + false, + ); + expect(WebauthnUtils.mapCredentialRegistrationResult).toHaveBeenCalledWith( + mockCreateCredentialsResult, + ); + }); + }); + + describe("get WebAuthn credentials", () => { + beforeEach(() => { + messenger.request = jest.fn().mockResolvedValue({ + type: MessageType.CredentialGetResponse, + result: mockCredentialAssertResult, + }); + }); + + it("gets and returns the WebAuthn credentials", async () => { + await navigator.credentials.get(mockCredentialRequestOptions); + + expect(WebauthnUtils.mapCredentialRequestOptions).toHaveBeenCalledWith( + mockCredentialRequestOptions, + false, + ); + expect(WebauthnUtils.mapCredentialAssertResult).toHaveBeenCalledWith( + mockCredentialAssertResult, + ); + }); + }); + + describe("destroy", () => { + it("should destroy the message listener when receiving a disconnect request", async () => { + jest.spyOn(globalThis.top, "removeEventListener"); + const SENDER = "bitwarden-webauthn"; + void messenger.handler({ type: MessageType.DisconnectRequest, SENDER, senderId: "1" }); + + expect(globalThis.top.removeEventListener).toHaveBeenCalledWith("focus", undefined); + expect(messenger.destroy).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.spec.ts b/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.spec.ts deleted file mode 100644 index 8f4efe0330..0000000000 --- a/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -describe("TriggerFido2ContentScriptInjection", () => { - afterEach(() => { - jest.resetModules(); - jest.clearAllMocks(); - }); - - describe("init", () => { - it("sends a message to the extension background", () => { - require("../content/trigger-fido2-content-script-injection"); - - expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ - command: "triggerFido2ContentScriptInjection", - }); - }); - }); -}); diff --git a/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.ts b/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.ts deleted file mode 100644 index 7ca6956729..0000000000 --- a/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.ts +++ /dev/null @@ -1,5 +0,0 @@ -(function () { - // 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 - chrome.runtime.sendMessage({ command: "triggerFido2ContentScriptInjection" }); -})(); diff --git a/apps/browser/src/vault/fido2/enums/fido2-content-script.enum.ts b/apps/browser/src/vault/fido2/enums/fido2-content-script.enum.ts new file mode 100644 index 0000000000..287de6804b --- /dev/null +++ b/apps/browser/src/vault/fido2/enums/fido2-content-script.enum.ts @@ -0,0 +1,10 @@ +export const Fido2ContentScript = { + PageScript: "content/fido2/page-script.js", + PageScriptAppend: "content/fido2/page-script-append-mv2.js", + ContentScript: "content/fido2/content-script.js", +} as const; + +export const Fido2ContentScriptId = { + PageScript: "fido2-page-script-registration", + ContentScript: "fido2-content-script-registration", +} as const; diff --git a/apps/browser/src/vault/fido2/enums/fido2-port-name.enum.ts b/apps/browser/src/vault/fido2/enums/fido2-port-name.enum.ts new file mode 100644 index 0000000000..7836247425 --- /dev/null +++ b/apps/browser/src/vault/fido2/enums/fido2-port-name.enum.ts @@ -0,0 +1,3 @@ +export const Fido2PortName = { + InjectedScript: "fido2-injected-content-script-port", +} as const; diff --git a/apps/browser/src/vault/popup/components/action-buttons.component.ts b/apps/browser/src/vault/popup/components/action-buttons.component.ts index 624789a5c0..b0e7b318d2 100644 --- a/apps/browser/src/vault/popup/components/action-buttons.component.ts +++ b/apps/browser/src/vault/popup/components/action-buttons.component.ts @@ -6,7 +6,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; +import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; 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"; @@ -31,7 +31,7 @@ export class ActionButtonsComponent implements OnInit, OnDestroy { private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private eventCollectionService: EventCollectionService, - private totpService: TotpService, + private totpService: TotpServiceAbstraction, private passwordRepromptService: PasswordRepromptService, private billingAccountProfileStateService: BillingAccountProfileStateService, ) {} diff --git a/apps/browser/src/vault/popup/components/fido2/fido2.component.ts b/apps/browser/src/vault/popup/components/fido2/fido2.component.ts index 81d1b88fd8..323d2ab4f2 100644 --- a/apps/browser/src/vault/popup/components/fido2/fido2.component.ts +++ b/apps/browser/src/vault/popup/components/fido2/fido2.component.ts @@ -311,7 +311,7 @@ export class Fido2Component implements OnInit, OnDestroy { } protected async search() { - this.hasSearched = this.searchService.isSearchable(this.searchText); + this.hasSearched = await this.searchService.isSearchable(this.searchText); this.searchPending = true; if (this.hasSearched) { this.displayedCiphers = await this.searchService.searchCiphers( diff --git a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts index b27a986231..a566b054c0 100644 --- a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts +++ b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts @@ -304,7 +304,7 @@ export class AddEditComponent extends BaseAddEditComponent { } private saveCipherState() { - return this.stateService.setAddEditCipherInfo({ + return this.cipherService.setAddEditCipherInfo({ cipher: this.cipher, collectionIds: this.collections == null diff --git a/apps/browser/src/vault/popup/components/vault/current-tab.component.html b/apps/browser/src/vault/popup/components/vault/current-tab.component.html index c971f6c937..0b2e16d09d 100644 --- a/apps/browser/src/vault/popup/components/vault/current-tab.component.html +++ b/apps/browser/src/vault/popup/components/vault/current-tab.component.html @@ -36,19 +36,43 @@ - -

{{ autofillCalloutText }}

+ +

+ {{ unassignedItemsBannerService.bannerText$ | async | i18n }} + {{ "unassignedItemsBannerCTAPartOne" | i18n }} + {{ "adminConsole" | i18n }} + {{ "unassignedItemsBannerCTAPartTwo" | i18n }} + {{ "learnMore" | i18n }} +

-

diff --git a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts index d9cf6550fa..4d2674fd70 100644 --- a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts +++ b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts @@ -1,13 +1,16 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { Router } from "@angular/router"; -import { Subject, firstValueFrom } from "rxjs"; -import { debounceTime, takeUntil } from "rxjs/operators"; +import { Subject, firstValueFrom, from } from "rxjs"; +import { debounceTime, switchMap, takeUntil } from "rxjs/operators"; +import { UnassignedItemsBannerService } from "@bitwarden/angular/services/unassigned-items-banner.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +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 { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -53,6 +56,11 @@ export class CurrentTabComponent implements OnInit, OnDestroy { private totpTimeout: number; private loadedTimeout: number; private searchTimeout: number; + private initPageDetailsTimeout: number; + + protected unassignedItemsBannerEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.UnassignedItemsBanner, + ); constructor( private platformUtilsService: PlatformUtilsService, @@ -70,6 +78,8 @@ export class CurrentTabComponent implements OnInit, OnDestroy { private organizationService: OrganizationService, private vaultFilterService: VaultFilterService, private vaultSettingsService: VaultSettingsService, + private configService: ConfigService, + protected unassignedItemsBannerService: UnassignedItemsBannerService, ) {} async ngOnInit() { @@ -120,8 +130,14 @@ export class CurrentTabComponent implements OnInit, OnDestroy { } this.search$ - .pipe(debounceTime(500), takeUntil(this.destroy$)) - .subscribe(() => this.searchVault()); + .pipe( + debounceTime(500), + switchMap(() => { + return from(this.searchVault()); + }), + takeUntil(this.destroy$), + ) + .subscribe(); const autofillOnPageLoadOrgPolicy = await firstValueFrom( this.autofillSettingsService.activateAutofillOnPageLoadFromPolicy$, @@ -232,14 +248,12 @@ export class CurrentTabComponent implements OnInit, OnDestroy { } } - searchVault() { - if (!this.searchService.isSearchable(this.searchText)) { + async searchVault() { + if (!(await this.searchService.isSearchable(this.searchText))) { return; } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/tabs/vault"], { queryParams: { searchText: this.searchText } }); + await this.router.navigate(["/tabs/vault"], { queryParams: { searchText: this.searchText } }); } closeOnEsc(e: KeyboardEvent) { @@ -303,18 +317,13 @@ export class CurrentTabComponent implements OnInit, OnDestroy { }); if (this.loginCiphers.length) { - void BrowserApi.tabSendMessage(this.tab, { - command: "collectPageDetails", - tab: this.tab, - sender: BroadcasterSubscriptionId, - }); - this.loginCiphers = this.loginCiphers.sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b), ); } this.isLoading = this.loaded = true; + this.collectTabPageDetails(); } async goToSettings() { @@ -352,4 +361,19 @@ export class CurrentTabComponent implements OnInit, OnDestroy { this.autofillCalloutText = this.i18nService.t("autofillSelectInfoWithoutCommand"); } } + + private collectTabPageDetails() { + void BrowserApi.tabSendMessage(this.tab, { + command: "collectPageDetails", + tab: this.tab, + sender: BroadcasterSubscriptionId, + }); + + window.clearTimeout(this.initPageDetailsTimeout); + this.initPageDetailsTimeout = window.setTimeout(() => { + if (this.pageDetails.length === 0) { + this.collectTabPageDetails(); + } + }, 250); + } } diff --git a/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts b/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts index 2510e2f966..deb4434df4 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts @@ -1,8 +1,8 @@ import { Location } from "@angular/common"; import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { firstValueFrom } from "rxjs"; -import { first } from "rxjs/operators"; +import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs"; +import { first, switchMap, takeUntil } from "rxjs/operators"; import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; @@ -53,7 +53,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy { folderCounts = new Map(); collectionCounts = new Map(); typeCounts = new Map(); - searchText: string; state: BrowserGroupingsComponentState; showLeftHeader = true; searchPending = false; @@ -71,6 +70,16 @@ export class VaultFilterComponent implements OnInit, OnDestroy { private hasSearched = false; private hasLoadedAllCiphers = false; private allCiphers: CipherView[] = null; + private destroy$ = new Subject(); + private _searchText$ = new BehaviorSubject(""); + private isSearchable: boolean = false; + + get searchText() { + return this._searchText$.value; + } + set searchText(value: string) { + this._searchText$.next(value); + } constructor( private i18nService: I18nService, @@ -148,6 +157,15 @@ export class VaultFilterComponent implements OnInit, OnDestroy { BrowserPopupUtils.setContentScrollY(window, this.state?.scrollY); } }); + + this._searchText$ + .pipe( + switchMap((searchText) => from(this.searchService.isSearchable(searchText))), + takeUntil(this.destroy$), + ) + .subscribe((isSearchable) => { + this.isSearchable = isSearchable; + }); } ngOnDestroy() { @@ -161,6 +179,8 @@ export class VaultFilterComponent implements OnInit, OnDestroy { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.saveState(); this.broadcasterService.unsubscribe(ComponentId); + this.destroy$.next(); + this.destroy$.complete(); } async load() { @@ -181,7 +201,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { async loadCiphers() { this.allCiphers = await this.cipherService.getAllDecrypted(); if (!this.hasLoadedAllCiphers) { - this.hasLoadedAllCiphers = !this.searchService.isSearchable(this.searchText); + this.hasLoadedAllCiphers = !(await this.searchService.isSearchable(this.searchText)); } await this.search(null); this.getCounts(); @@ -210,7 +230,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { } const filterDeleted = (c: CipherView) => !c.isDeleted; if (timeout == null) { - this.hasSearched = this.searchService.isSearchable(this.searchText); + this.hasSearched = this.isSearchable; this.ciphers = await this.searchService.searchCiphers( this.searchText, filterDeleted, @@ -223,7 +243,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { } this.searchPending = true; this.searchTimeout = setTimeout(async () => { - this.hasSearched = this.searchService.isSearchable(this.searchText); + this.hasSearched = this.isSearchable; if (!this.hasLoadedAllCiphers && !this.hasSearched) { await this.loadCiphers(); } else { @@ -381,9 +401,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { } showSearching() { - return ( - this.hasSearched || (!this.searchPending && this.searchService.isSearchable(this.searchText)) - ); + return this.hasSearched || (!this.searchPending && this.isSearchable); } closeOnEsc(e: KeyboardEvent) { diff --git a/apps/browser/src/vault/popup/components/vault/view.component.ts b/apps/browser/src/vault/popup/components/vault/view.component.ts index d7ef15afb7..a225db0c11 100644 --- a/apps/browser/src/vault/popup/components/vault/view.component.ts +++ b/apps/browser/src/vault/popup/components/vault/view.component.ts @@ -20,7 +20,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; +import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; @@ -74,7 +74,7 @@ export class ViewComponent extends BaseViewComponent { constructor( cipherService: CipherService, folderService: FolderService, - totpService: TotpService, + totpService: TotpServiceAbstraction, tokenService: TokenService, i18nService: I18nService, cryptoService: CryptoService, diff --git a/apps/browser/src/vault/services/abstractions/fido2.service.ts b/apps/browser/src/vault/services/abstractions/fido2.service.ts deleted file mode 100644 index 138b538b15..0000000000 --- a/apps/browser/src/vault/services/abstractions/fido2.service.ts +++ /dev/null @@ -1,4 +0,0 @@ -export abstract class Fido2Service { - init: () => Promise; - injectFido2ContentScripts: (sender: chrome.runtime.MessageSender) => Promise; -} diff --git a/apps/browser/src/vault/services/fido2.service.spec.ts b/apps/browser/src/vault/services/fido2.service.spec.ts deleted file mode 100644 index 1db2bdfb77..0000000000 --- a/apps/browser/src/vault/services/fido2.service.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { BrowserApi } from "../../platform/browser/browser-api"; - -import Fido2Service from "./fido2.service"; - -describe("Fido2Service", () => { - let fido2Service: Fido2Service; - let tabMock: chrome.tabs.Tab; - let sender: chrome.runtime.MessageSender; - - beforeEach(() => { - fido2Service = new Fido2Service(); - tabMock = { id: 123, url: "https://bitwarden.com" } as chrome.tabs.Tab; - sender = { tab: tabMock }; - jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation(); - }); - - afterEach(() => { - jest.resetModules(); - jest.clearAllMocks(); - }); - - describe("injectFido2ContentScripts", () => { - const fido2ContentScript = "content/fido2/content-script.js"; - const defaultExecuteScriptOptions = { runAt: "document_start" }; - - it("accepts an extension message sender and injects the fido2 scripts into the tab of the sender", async () => { - await fido2Service.injectFido2ContentScripts(sender); - - expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, { - file: fido2ContentScript, - ...defaultExecuteScriptOptions, - }); - }); - }); -}); diff --git a/apps/browser/src/vault/services/fido2.service.ts b/apps/browser/src/vault/services/fido2.service.ts deleted file mode 100644 index 98b440b109..0000000000 --- a/apps/browser/src/vault/services/fido2.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { BrowserApi } from "../../platform/browser/browser-api"; - -import { Fido2Service as Fido2ServiceInterface } from "./abstractions/fido2.service"; - -export default class Fido2Service implements Fido2ServiceInterface { - async init() { - const tabs = await BrowserApi.tabsQuery({}); - tabs.forEach((tab) => { - if (tab.url?.startsWith("https")) { - // 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.injectFido2ContentScripts({ tab } as chrome.runtime.MessageSender); - } - }); - - BrowserApi.addListener(chrome.runtime.onConnect, (port) => { - if (port.name === "fido2ContentScriptReady") { - port.postMessage({ command: "fido2ContentScriptInit" }); - } - }); - } - - /** - * Injects the FIDO2 content script into the current tab. - * @param {chrome.runtime.MessageSender} sender - * @returns {Promise} - */ - async injectFido2ContentScripts(sender: chrome.runtime.MessageSender): Promise { - await BrowserApi.executeScriptInTab(sender.tab.id, { - file: "content/fido2/content-script.js", - frameId: sender.frameId, - runAt: "document_start", - }); - } -} diff --git a/apps/browser/store/locales/ar/copy.resx b/apps/browser/store/locales/ar/copy.resx index e74606ff15..e1bfa48b44 100644 --- a/apps/browser/store/locales/ar/copy.resx +++ b/apps/browser/store/locales/ar/copy.resx @@ -1,17 +1,17 @@  - @@ -118,42 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - مدير كلمات مرور مجاني + Bitwarden Password Manager - مدير كلمات مرور مجاني وآمن لجميع أجهزتك + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - شركة Bitwarden, Inc هي الشركة الأم لشركة 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -تم تصنيف Bitwarden كأفصل مدير كلمات مرور بواسطة كل من The Verge، U.S News & World Report، CNET، وغيرهم. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -قم بادراة وحفظ وتأمين كلمات المرور الخاصة بك، ومشاركتها بين اجهزتك من اي مكان. -يوفر Bitwarden حل مفتوح المصدر لادارة كلمات المرور للجميع، سواء في المنزل، في العمل او في اي مكان. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -قم بانشاء كلمات مرور قوية وفريدة وعشوائية حسب متطلبات الأمان للصفحات التي تزورها. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -يوفر Bitwarden Send امكانية ارسال البيانات --- النصوص والملفات --- بطريقة مشفرة وسريعة لأي شخص. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -يوفر Bitwarden خطط خاصة للفرق والشركات والمؤسسات لتمكنك من مشاركة كلمات المرور مع زملائك في العمل. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -لماذا قد تختار Bitwarden: -تشفير على مستوى عالمي -كلمات المرور محمية بتشفير متقدم تام (end-to-end encryption) من نوعية AES-256 bit، مع salted hashing، و PBKDF2 SHA-256. كل هذا لابقاء بياناتك محمية وخاصة. +More reasons to choose Bitwarden: -مولد كلمات المرور المدمج -قم بانشاء كلمات مرور قوية وفريدة وعشوائية حسب متطلبات الأمان للصفحات التي تزورها. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -الترجمات العالمية -يتوفر Bitwarden باكثر من 40 لغة، وتتنامى الترجمات بفضل مجتمعنا العالمي. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -تطبيقات متعددة المنصات -قم بحماية ومشاركة بياناتاك الحساسة عبر خزنة Bitwarden من اي متصفح ويب، او هاتف ذكي، او جهاز كمبيوتر، وغيرها. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - مدير كلمات مرور مجاني وآمن لجميع أجهزتك + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. مزامنة خزنتك والوصول إليها من عدة أجهزة diff --git a/apps/browser/store/locales/az/copy.resx b/apps/browser/store/locales/az/copy.resx index cb05f8e5d9..2a3d507df2 100644 --- a/apps/browser/store/locales/az/copy.resx +++ b/apps/browser/store/locales/az/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Ödənişsiz Parol Meneceri + Bitwarden Password Manager - Bütün cihazlarınız üçün güvənli və ödənişsiz bir parol meneceri + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc., 8bit Solutions LLC-nin ana şirkətidir. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS & WORLD REPORT, CNET VƏ BİR ÇOXUNA GÖRƏ ƏN YAXŞI PAROL MENECERİDİR. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Hər yerdən limitsiz cihazda limitsiz parolu idarə edin, saxlayın, qoruyun və paylaşın. Bitwarden evdə, işdə və ya yolda hər kəsə açıq mənbəli parol idarəetmə həllərini təqdim edir. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Çox istifadə etdiyiniz hər veb sayt üçün təhlükəsizlik tələblərinə görə güclü, unikal və təsadüfi parollar yaradın. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send şifrələnmiş məlumatların (fayl və sadə mətnləri) birbaşa və sürətli göndərilməsini təmin edir. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden, parolları iş yoldaşlarınızla təhlükəsiz paylaşa bilməyiniz üçün şirkətlərə Teams və Enterprise planları təklif edir. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Nəyə görə Bitwarden-i seçməliyik: -Yüksək səviyyə şifrələmə -Parollarınız qabaqcıl ucdan-uca şifrələmə (AES-256 bit, salted hashtag və PBKDF2 SHA-256) ilə qorunur, beləcə datanızın güvənli və gizli qalmasını təmin edir. +More reasons to choose Bitwarden: -Daxili parol yaradıcı -Çox istifadə etdiyiniz hər veb sayt üçün təhlükəsizlik tələblərinə görə güclü, unikal və təsadüfi şifrələr yaradın. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Qlobal tərcümələr -Bitwarden tərcümələri 40 dildə mövcuddur və qlobal cəmiyyətimiz sayəsində böyüməyə davam edir. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Çarpaz platform tətbiqləri -Bitwarden anbarındakı həssas verilənləri, istənilən brauzerdən, mobil cihazdan və ya masaüstü əməliyyat sistemindən və daha çoxundan qoruyub paylaşın. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Bütün cihazlarınız üçün güvənli və ödənişsiz bir parol meneceri + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Anbarınıza bir neçə cihazdan eyniləşdirərək müraciət edin diff --git a/apps/browser/store/locales/be/copy.resx b/apps/browser/store/locales/be/copy.resx index f84dd699a7..65c337826b 100644 --- a/apps/browser/store/locales/be/copy.resx +++ b/apps/browser/store/locales/be/copy.resx @@ -1,17 +1,17 @@  - @@ -118,24 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – бясплатны менеджар пароляў + Bitwarden Password Manager - Бяспечны і бясплатны менеджар пароляў для ўсіх вашых прылад + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden - просты і бяспечны спосаб захоўваць усе вашы імёны карыстальніка і паролі, а таксама лёгка іх сінхранізаваць паміж усімі вашымі прыладамі. Пашырэнне праграмы Bitwarden дазваляе хутка ўвайсці на любы вэб-сайт з дапамогай Safari або Chrome і падтрымліваецца сотнямі іншых папулярных праграм. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -Крадзеж пароляў — сур'ёзная праблема. Сайты і праграмы, якія вы выкарыстоўваеце падвяргаюцца нападам кожны дзень. Праблемы ў іх бяспецы могуць прывесці да крадзяжу вашага пароля. Акрамя таго, калі вы выкарыстоўваеце адны і тыя ж паролі на розных сайтах і праграмах, то хакеры могуць лёгка атрымаць доступ да некалькіх вашых уліковых запісаў адразу (да паштовай скрыні, да банкаўскага рахунку ды г. д.). +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Эксперты па бяспецы рэкамендуюць выкарыстоўваць розныя выпадкова знегерыраваныя паролі для кожнага створанага вамі ўліковага запісу. Але як жа кіраваць усімі гэтымі паролямі? Bitwarden дазваляе вам лёгка атрымаць доступ да вашых пароляў, а гэтак жа ствараць і захоўваць іх. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Bitwarden захоўвае ўсе вашы імёны карыстальніка і паролі ў зашыфраваным сховішчы, якое сінхранізуецца паміж усімі вашымі прыладамі. Да таго, як даныя пакінуць вашу прыладу, яны будуць зашыфраваны і толькі потым адпраўлены. Мы ў Bitwarden не зможам прачытаць вашы даныя, нават калі мы гэтага захочам. Вашы даныя зашыфраваны пры дапамозе алгарытму AES-256 і PBKDF2 SHA-256. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden — гэта праграмнае забеспячэнне з адкрытым на 100% зыходным кодам. Зыходны код Bitwarden размешчаны на GitHub, і кожны можа свабодна праглядаць, правяраць і рабіць унёсак у код Bitwarden. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. + +Use Bitwarden to secure your workforce and share sensitive information with colleagues. + + +More reasons to choose Bitwarden: + +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. + +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. + +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - Бяспечны і бясплатны менеджар пароляў для ўсіх вашых прылад + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Сінхранізацыя і доступ да сховішча з некалькіх прылад diff --git a/apps/browser/store/locales/bg/copy.resx b/apps/browser/store/locales/bg/copy.resx index 29c468f045..bc08f6a107 100644 --- a/apps/browser/store/locales/bg/copy.resx +++ b/apps/browser/store/locales/bg/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden — безплатен управител на пароли + Bitwarden Password Manager - Сигурен и свободен управител на пароли за всички устройства + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - „Bitwarden, Inc.“ е компанията-майка на „8bit Solutions LLC“. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -ОПРЕДЕЛЕН КАТО НАЙ-ДОБРИЯТ УПРАВИТЕЛ НА ПАРОЛИ ОТ „THE VERGE“, „U.S. NEWS & WORLD REPORT“, „CNET“ И ОЩЕ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Управлявайте, съхранявайте, защитавайте и споделяйте неограничен брой пароли на неограничен брой устройства от всяка точка на света. Битуорден предоставя решение за управление на паролите с отворен код, от което може да се възползва всеки, било то у дома, на работа или на път. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Създавайте сигурни, уникални и случайни пароли според изискванията за сигурност на всеки уеб сайт, който посещавате. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -С Изпращанията на Битуорден можете незабавно да предавате шифрована информация под формата на файлове и обикновен текст – директно и с всекиго. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Битуорден предлага планове за екипи и големи фирми, така че служителите в компаниите да могат безопасно да споделят пароли помежду си. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Защо да изберете Битуорден: -Шифроване от най-висока класа -Паролите са защитени със сложен шифър „от край до край“ (AES-256 bit, salted hashtag и PBKDF2 SHA-256), така че данните Ви остават да са защитени и неприкосновени. +More reasons to choose Bitwarden: -Вграден генератор на пароли -Създавайте сигурни, уникални и случайни пароли според изискванията за сигурност на всеки уеб сайт, който посещавате. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Глобални преводи -Битуорден е преведен на 40 езика и този брой не спира да расте, благодарение на нашата глобална общност. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Приложения за всяка система -Защитете и споделяйте поверителните данни от своя трезор в Битуорден от всеки браузър, мобилно устройство или компютър. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Сигурен и свободен управител на пароли за всички устройства + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Удобен достъп до трезора, който се синхронизира от всички устройства diff --git a/apps/browser/store/locales/bn/copy.resx b/apps/browser/store/locales/bn/copy.resx index a8eb4f7c75..1bcfb19001 100644 --- a/apps/browser/store/locales/bn/copy.resx +++ b/apps/browser/store/locales/bn/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – বিনামূল্যের পাসওয়ার্ড ব্যবস্থাপক + Bitwarden Password Manager - আপনার সমস্ত ডিভাইসের জন্য একটি সুরক্ষিত এবং বিনামূল্যের পাসওয়ার্ড ব্যবস্থাপক + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - আপনার সমস্ত ডিভাইসের জন্য একটি সুরক্ষিত এবং বিনামূল্যের পাসওয়ার্ড ব্যবস্থাপক + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. একাধিক ডিভাইস থেকে আপনার ভল্ট সিঙ্ক এবং ব্যাবহার করুন diff --git a/apps/browser/store/locales/bs/copy.resx b/apps/browser/store/locales/bs/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/bs/copy.resx +++ b/apps/browser/store/locales/bs/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/ca/copy.resx b/apps/browser/store/locales/ca/copy.resx index 0bd454addb..27e685841b 100644 --- a/apps/browser/store/locales/ca/copy.resx +++ b/apps/browser/store/locales/ca/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Administrador de contrasenyes gratuït + Bitwarden Password Manager - Administrador de contrasenyes segur i gratuït per a tots els vostres dispositius + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. és la companyia matriu de solucions de 8 bits LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -Nomenada Millor gestor de contrasenyes per THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gestioneu, emmagatzemeu, segures i compartiu contrasenyes il·limitades a través de dispositius il·limitats des de qualsevol lloc. Bitwarden lliura solucions de gestió de contrasenyes de codi obert a tothom, ja siga a casa, a la feina o sobre la marxa. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Genereu contrasenyes fortes, úniques i aleatòries basades en els requisits de seguretat per a cada lloc web que freqüenteu. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send transmet ràpidament informació xifrada --- Fitxers i text complet - directament a qualsevol persona. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden ofereix equips i plans empresarials per a empreses perquè pugueu compartir amb seguretat contrasenyes amb els companys. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Per què triar Bitwarden: -Xifratge de classe mundial -Les contrasenyes estan protegides amb un xifratge avançat fi-a-fi (AES-256 bit, salted hashtag, and PBKDF2 SHA-256), de manera que les vostres dades es mantenen segures i privades. +More reasons to choose Bitwarden: -Generador de contrasenyes integrat -Genereu contrasenyes fortes, úniques i aleatòries basades en els requisits de seguretat per a cada lloc web que freqüenteu. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traduccions globals -Les traduccions de Bitwarden existeixen en 40 idiomes i creixen, gràcies a la nostra comunitat global. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplicacions de plataforma creuada -Assegureu-vos i compartiu dades sensibles a la vostra caixa forta de Bitwarden des de qualsevol navegador, dispositiu mòbil o S.O. d'escriptori, i molt més. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Administrador de contrasenyes segur i gratuït per a tots els vostres dispositius + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sincronitzeu i accediu a la vostra caixa forta des de diversos dispositius diff --git a/apps/browser/store/locales/cs/copy.resx b/apps/browser/store/locales/cs/copy.resx index 6b711e2863..59d8c60b40 100644 --- a/apps/browser/store/locales/cs/copy.resx +++ b/apps/browser/store/locales/cs/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Bezplatný správce hesel + Bitwarden Password Manager - Bezpečný a bezplatný správce hesel pro všechna Vaše zařízení + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. je mateřskou společností 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS & WORLD REPORT, CNET A DALŠÍ JI OZNAČILY ZA NEJLEPŠÍHO SPRÁVCE HESEL. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Spravujte, ukládejte, zabezpečujte a sdílejte neomezený počet hesel na neomezeném počtu zařízení odkudkoliv. Bitwarden poskytuje open source řešení pro správu hesel všem, ať už doma, v práci nebo na cestách. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generujte silná, jedinečná a náhodná hesla na základě bezpečnostních požadavků pro každou webovou stránku, kterou navštěvujete. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send rychle přenáší šifrované informace --- soubory a prostý text -- přímo a komukoli. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden nabízí plány Teams a Enterprise pro firmy, takže můžete bezpečně sdílet hesla s kolegy. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Proč si vybrat Bitwarden: -Šifrování na světové úrovni -Hesla jsou chráněna pokročilým koncovým šifrováním (AES-256 bit, salted hashování a PBKDF2 SHA-256), takže Vaše data zůstanou bezpečná a soukromá. +More reasons to choose Bitwarden: -Vestavěný generátor hesel -Generujte silná, jedinečná a náhodná hesla na základě bezpečnostních požadavků pro každou webovou stránku, kterou navštěvujete. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globální překlady -Překlady Bitwarden existují ve 40 jazycích a díky naší globální komunitě se stále rozšiřují. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplikace pro více platforem -Zabezpečte a sdílejte citlivá data v rámci svého trezoru Bitwarden z jakéhokoli prohlížeče, mobilního zařízení nebo operačního systému pro počítač. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Bezpečný a bezplatný správce hesel pro všechna Vaše zařízení + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synchronizujte a přistupujte ke svému trezoru z různých zařízení diff --git a/apps/browser/store/locales/cy/copy.resx b/apps/browser/store/locales/cy/copy.resx index 8222329630..983a112c07 100644 --- a/apps/browser/store/locales/cy/copy.resx +++ b/apps/browser/store/locales/cy/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Rheolydd cyfineiriau am ddim + Bitwarden Password Manager - Rheolydd diogel a rhad ac am ddim ar gyfer eich holl ddyfeisiau + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. yw rhiant-gwmni 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -Y RHEOLYDD CYFRINEIRIAU GORAU YN ÔL THE VERGE, US NEWS & WORLD REPORT, CNET, A MWY. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Rheolwch, storiwch, diogelwch a rhannwch gyfrineiriau di-ri ar draws dyfeiriau di-ri o unrhyw le. Mae Bitwarden yn cynnig rhaglenni rheoli cyfrineiriau cod-agored i bawb, boed gartref, yn y gwaith, neu ar fynd. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Gallwch gynhyrchu cyfrineiriau cryf, unigryw ac ar hap yn seiliedig ar ofynion diogelwch ar gyfer pob gwefan rydych chi'n ei defnyddio. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Mae Bitwarden Send yn trosglwyddo gwybodaeth wedi'i hamgryptio yn gyflym -- ffeiliau a thestun plaen -- yn uniongyrchol i unrhyw un. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Mae Bitwarden yn cynnig cynlluniau Teams ac Enterprise i gwmnïau er mwyn i chi allu rhannu cyfrineiriau gyda chydweithwyr yn ddiogel. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Pam dewis Bitwarden: -Amgryptio o'r radd flaenaf -Mae cyfrineiriau wedi'u hamddiffyn ag amgryptio datblygedig o un pen i'r llall (AES-256 bit, hashio â halen, a PBKDF2 SHA-256) er mwyn i'ch data aros yn ddiogel ac yn breifat. +More reasons to choose Bitwarden: -Cynhyrchydd cyfrineiriau -Gallwch gynhyrchu cyfrineiriau cryf, unigryw ac ar hap yn seiliedig ar ofynion diogelwch ar gyfer pob gwefan rydych chi'n ei defnyddio. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Ar gael yn eich iaith chi -Mae Bitwarden wedi'i gyfieithu i dros 40 o ieithoedd, diolch i'n cymuned fyd-eang. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Rhaglenni traws-blatfform -Diogelwch a rhannwch ddata sensitif yn eich cell Bitwarden o unrhyw borwr, dyfais symudol, neu system weithredu, a mwy. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Rheolydd diogel a rhad ac am ddim ar gyfer eich holl ddyfeisiau + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Gallwch gael mynediad at, a chysoni, eich cell o sawl dyfais diff --git a/apps/browser/store/locales/da/copy.resx b/apps/browser/store/locales/da/copy.resx index 858c56dea9..775a3edd81 100644 --- a/apps/browser/store/locales/da/copy.resx +++ b/apps/browser/store/locales/da/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Gratis adgangskodemanager + Bitwarden Password Manager - En sikker og gratis adgangskodemanager til alle dine enheder + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. er moderselskab for 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -UDNÆVNT BEDSTE PASSWORD MANAGER AF THE VERGE, U.S. NEWS & WORLD REPORT, CNET OG FLERE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Administrér, gem, sikr og del adgangskoder ubegrænset på tværs af enheder hvor som helst. Bitwarden leverer open source adgangskodeadministrationsløsninger til alle, hvad enten det er hjemme, på arbejdspladsen eller på farten. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generér stærke, unikke og tilfældige adgangskoder baseret på sikkerhedskrav til hvert websted, du besøger. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send overfører hurtigt krypterede oplysninger --- filer og almindelig tekst - direkte til enhver. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden tilbyder Teams og Enterprise-planer for virksomheder, så du sikkert kan dele adgangskoder med kolleger. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Hvorfor vælge Bitwarden: -Kryptering i verdensklasse -Adgangskoder er beskyttet med avanceret end-to-end-kryptering (AES-256 bit, saltet hashing og PBKDF2 SHA-256), så dine data forbliver sikre og private. +More reasons to choose Bitwarden: -Indbygget adgangskodegenerator -Generér stærke, unikke og tilfældige adgangskoder baseret på sikkerhedskrav til hvert websted, du besøger. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globale oversættelser -Bitwarden findes på 40 sprog, og flere kommer til, takket være vores globale fællesskab. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Applikationer på tværs af platforme -Beskyt og del følsomme data i din Bitwarden boks fra enhver browser, mobilenhed eller desktop OS og mere. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - En sikker og gratis adgangskodemanager til alle dine enheder + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synkroniser og få adgang til din boks fra flere enheder diff --git a/apps/browser/store/locales/de/copy.resx b/apps/browser/store/locales/de/copy.resx index 139a6026fd..2267c6c85e 100644 --- a/apps/browser/store/locales/de/copy.resx +++ b/apps/browser/store/locales/de/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Kostenloser Passwort-Manager + Bitwarden Passwort-Manager - Ein sicherer und kostenloser Passwort-Manager für all deine Geräte + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. ist die Muttergesellschaft von 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -AUSGEZEICHNET ALS BESTER PASSWORTMANAGER VON THE VERGE, U.S. NEWS & WORLD REPORT, CNET UND ANDEREN. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Verwalte, speichere, sichere und teile unbegrenzte Passwörter von überall auf unbegrenzten Geräten. Bitwarden liefert Open-Source-Passwort-Management-Lösungen für alle, sei es zu Hause, am Arbeitsplatz oder unterwegs. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generiere starke, einzigartige und zufällige Passwörter basierend auf Sicherheitsanforderungen für jede Website, die du häufig besuchst. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send überträgt schnell verschlüsselte Informationen - Dateien und Klartext - direkt an jeden. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden bietet Teams und Enterprise Pläne für Unternehmen an, damit du Passwörter sicher mit Kollegen teilen kannst. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Warum Bitwarden: -Weltklasse Verschlüsselung -Passwörter sind durch erweiterte Ende-zu-Ende-Verschlüsselung (AES-256 Bit, salted hashing und PBKDF2 SHA-256) so bleiben deine Daten sicher und privat. +More reasons to choose Bitwarden: -Integrierter Passwortgenerator -Generiere starke, einzigartige und zufällige Passwörter basierend auf Sicherheitsanforderungen für jede Website, die du häufig besuchst. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globale Übersetzungen -Bitwarden Übersetzungen existieren in 40 Sprachen und wachsen dank unserer globalen Community. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Plattformübergreifende Anwendungen -Sichere und teile vertrauliche Daten in deinem Bitwarden Tresor von jedem Browser, jedem mobilen Gerät oder Desktop-Betriebssystem und mehr. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Ein sicherer und kostenloser Passwort-Manager für all deine Geräte + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synchronisiere und greife auf deinen Tresor von unterschiedlichen Geräten aus zu diff --git a/apps/browser/store/locales/el/copy.resx b/apps/browser/store/locales/el/copy.resx index 01def6ea5a..fb50f95bdc 100644 --- a/apps/browser/store/locales/el/copy.resx +++ b/apps/browser/store/locales/el/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Δωρεάν Διαχειριστής Κωδικών + Bitwarden Password Manager - Ένας ασφαλής και δωρεάν διαχειριστής κωδικών για όλες τις συσκευές σας + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Η Bitwarden, Inc. είναι η μητρική εταιρεία της 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -ΟΝΟΜΑΣΘΗΚΕ ΩΣ Ο ΚΑΛΥΤΕΡΟΣ ΔΙΑΧΕΙΡΙΣΤΗΣ ΚΩΔΙΚΩΝ ΠΡΟΣΒΑΣΗΣ ΑΠΟ ΤΟ VERGE, το U.S. NEWS & WORLD REPORT, το CNET και άλλα. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Διαχειριστείτε, αποθηκεύστε, ασφαλίστε και μοιραστείτε απεριόριστους κωδικούς πρόσβασης σε απεριόριστες συσκευές από οπουδήποτε. Το Bitwarden παρέχει λύσεις διαχείρισης κωδικών πρόσβασης ανοιχτού κώδικα σε όλους, στο σπίτι, στη δουλειά ή εν κινήσει. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Δημιουργήστε ισχυρούς, μοναδικούς και τυχαίους κωδικούς πρόσβασης βάσει των απαιτήσεων ασφαλείας, για κάθε ιστότοπο που συχνάζετε. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Το Bitwarden Send αποστέλλει γρήγορα κρυπτογραφημένες πληροφορίες --- αρχεία και απλό κείμενο -- απευθείας σε οποιονδήποτε. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Το Bitwarden προσφέρει προγράμματα Teams και Enterprise για εταιρείες, ώστε να μπορείτε να μοιράζεστε με ασφάλεια τους κωδικούς πρόσβασης με τους συναδέλφους σας. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Γιατί να επιλέξετε το Bitwarden: -Κρυπτογράφηση παγκόσμιας κλάσης -Οι κωδικοί πρόσβασης προστατεύονται με προηγμένη end-to-end κρυπτογράφηση (AES-256 bit, salted hashing και PBKDF2 SHA-256), ώστε τα δεδομένα σας να παραμένουν ασφαλή και ιδιωτικά. +More reasons to choose Bitwarden: -Ενσωματωμένη Γεννήτρια Κωδικών Πρόσβασης -Δημιουργήστε ισχυρούς, μοναδικούς και τυχαίους κωδικούς πρόσβασης βάσει των απαιτήσεων ασφαλείας, για κάθε ιστότοπο που συχνάζετε. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Παγκόσμιες Μεταφράσεις -Υπάρχουν μεταφράσεις για το Bitwarden σε 40 γλώσσες και αυξάνονται συνεχώς, χάρη στην παγκόσμια κοινότητά μας. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Εφαρμογές για όλες τις πλατφόρμες -Ασφαλίστε και μοιραστείτε ευαίσθητα δεδομένα εντός του Bitwarden Vault από οποιοδήποτε πρόγραμμα περιήγησης, κινητή συσκευή ή λειτουργικό σύστημα υπολογιστών, και πολλά άλλα. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Ένας ασφαλής και δωρεάν διαχειριστής κωδικών για όλες τις συσκευές σας + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Συγχρονίστε και αποκτήστε πρόσβαση στο θησαυροφυλάκιό σας από πολλαπλές συσκευές diff --git a/apps/browser/store/locales/en/copy.resx b/apps/browser/store/locales/en/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/en/copy.resx +++ b/apps/browser/store/locales/en/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/en_GB/copy.resx b/apps/browser/store/locales/en_GB/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/en_GB/copy.resx +++ b/apps/browser/store/locales/en_GB/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/en_IN/copy.resx b/apps/browser/store/locales/en_IN/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/en_IN/copy.resx +++ b/apps/browser/store/locales/en_IN/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/es/copy.resx b/apps/browser/store/locales/es/copy.resx index dc7484777a..472697d825 100644 --- a/apps/browser/store/locales/es/copy.resx +++ b/apps/browser/store/locales/es/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Gestor de contraseñas gratuito + Bitwarden Password Manager - Un gestor de contraseñas seguro y gratuito para todos tus dispositivos + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. es la empresa matriz de 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMBRADO MEJOR ADMINISTRADOR DE CONTRASEÑAS POR THE VERGE, U.S. NEWS & WORLD REPORT, CNET Y MÁS. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Administre, almacene, proteja y comparta contraseñas ilimitadas en dispositivos ilimitados desde cualquier lugar. Bitwarden ofrece soluciones de gestión de contraseñas de código abierto para todos, ya sea en casa, en el trabajo o en mientras estás de viaje. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Genere contraseñas seguras, únicas y aleatorias en función de los requisitos de seguridad de cada sitio web que frecuenta. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send transmite rápidamente información cifrada --- archivos y texto sin formato, directamente a cualquier persona. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden ofrece planes Teams y Enterprise para empresas para que pueda compartir contraseñas de forma segura con colegas. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -¿Por qué elegir Bitwarden? -Cifrado de clase mundial -Las contraseñas están protegidas con cifrado avanzado de extremo a extremo (AES-256 bits, salted hashing y PBKDF2 SHA-256) para que sus datos permanezcan seguros y privados. +More reasons to choose Bitwarden: -Generador de contraseñas incorporado -Genere contraseñas fuertes, únicas y aleatorias en función de los requisitos de seguridad de cada sitio web que frecuenta. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traducciones Globales -Las traducciones de Bitwarden existen en 40 idiomas y están creciendo, gracias a nuestra comunidad global. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplicaciones multiplataforma -Proteja y comparta datos confidenciales dentro de su Caja Fuerte de Bitwarden desde cualquier navegador, dispositivo móvil o sistema operativo de escritorio, y más. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Un gestor de contraseñas seguro y gratuito para todos tus dispositivos + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sincroniza y accede a tu caja fuerte desde múltiples dispositivos diff --git a/apps/browser/store/locales/et/copy.resx b/apps/browser/store/locales/et/copy.resx index 2014ec88a8..eccbeba1ed 100644 --- a/apps/browser/store/locales/et/copy.resx +++ b/apps/browser/store/locales/et/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Tasuta paroolihaldur + Bitwarden Password Manager - Tasuta ja turvaline paroolihaldur kõikidele sinu seadmetele + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Tasuta ja turvaline paroolihaldur kõikidele Sinu seadmetele + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sünkroniseeri ja halda oma kontot erinevates seadmetes diff --git a/apps/browser/store/locales/eu/copy.resx b/apps/browser/store/locales/eu/copy.resx index e5b3d542e3..e4271e8ae3 100644 --- a/apps/browser/store/locales/eu/copy.resx +++ b/apps/browser/store/locales/eu/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden — Doaneko pasahitz kudeatzailea + Bitwarden Password Manager - Bitwarden, zure gailu guztietarako pasahitzen kudeatzaile seguru eta doakoa + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. 8bit Solutions LLC-ren enpresa matrizea da. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS & WORLD REPORT ETA CNET ENPRESEK PASAHITZ-ADMINISTRATZAILE ONENA izendatu dute, besteak beste. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gailu guztien artean pasahitz mugagabeak kudeatu, biltegiratu, babestu eta partekatzen ditu. Bitwardenek kode irekiko pasahitzak administratzeko irtenbideak eskaintzen ditu, bai etxean, bai lanean, bai bidaiatzen ari zaren bitartean. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Pasahitz sendoak, bakarrak eta ausazkoak sortzen ditu, webgune bakoitzaren segurtasun-baldintzetan oinarrituta. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send-ek azkar transmititzen du zifratutako informazioa --- artxiboak eta testu sinplea -- edozein pertsonari zuzenean. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden-ek Taldeak eta Enpresak planak eskaintzen ditu, enpresa bereko lankideek pasahitzak modu seguruan parteka ditzaten. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Zergatik aukeratu Bitwarden: -Mundu-mailako zifratzea -Pasahitzak muturretik muturrerako zifratze aurreratuarekin babestuta daude (AES-256 bit, salted hashtag eta PBKDF2 SHA-256), zure informazioa seguru eta pribatu egon dadin. +More reasons to choose Bitwarden: -Pasahitzen sortzailea -Pasahitz sendoak, bakarrak eta ausazkoak sortzen ditu, web gune bakoitzaren segurtasun-baldintzetan oinarrituta. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Itzulpenak -Bitwarden 40 hizkuntzatan dago, eta gero eta gehiago dira, gure komunitate globalari esker. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Plataforma anitzeko aplikazioak -Babestu eta partekatu zure Bitwarden kutxa gotorraren informazio konfidentziala edozein nabigatzailetatik, gailu mugikorretatik, mahaigaineko aplikaziotik eta gehiagotatik. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Zure gailu guztietarako pasahitzen kudeatzaile seguru eta doakoa + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sinkronizatu eta sartu zure kutxa gotorrean hainbat gailutatik diff --git a/apps/browser/store/locales/fa/copy.resx b/apps/browser/store/locales/fa/copy.resx index 23cb3f3bf0..67095435ac 100644 --- a/apps/browser/store/locales/fa/copy.resx +++ b/apps/browser/store/locales/fa/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - مدیریت کلمه عبور رایگان + Bitwarden Password Manager - یک مدیریت کننده کلمه عبور رایگان برای تمامی دستگاه‌هایتان + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden، Inc. شرکت مادر 8bit Solutions LLC است. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -به عنوان بهترین مدیر کلمه عبور توسط VERGE، US News & WORLD REPORT، CNET و دیگران انتخاب شد. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -کلمه‌های عبور با تعداد نامحدود را در دستگاه‌های نامحدود از هر کجا مدیریت کنید، ذخیره کنید، ایمن کنید و به اشتراک بگذارید. Bitwarden راه حل های مدیریت رمز عبور منبع باز را به همه ارائه می دهد، چه در خانه، چه در محل کار یا در حال حرکت. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -بر اساس الزامات امنیتی برای هر وب سایتی که بازدید می کنید، کلمه‌های عبور قوی، منحصر به فرد و تصادفی ایجاد کنید. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send به سرعت اطلاعات رمزگذاری شده --- فایل ها و متن ساده - را مستقیماً به هر کسی منتقل می کند. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden برنامه‌های Teams و Enterprise را برای شرکت‌ها ارائه می‌دهد تا بتوانید به‌طور ایمن کلمه‌های را با همکاران خود به اشتراک بگذارید. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -چرا Bitwarden را انتخاب کنید: -رمزگذاری در کلاس جهانی -کلمه‌های عبور با رمزگذاری پیشرفته (AES-256 بیت، هشتگ سالت و PBKDF2 SHA-256) محافظت می‌شوند تا داده‌های شما امن و خصوصی بمانند. +More reasons to choose Bitwarden: -تولیدکننده کلمه عبور داخلی -بر اساس الزامات امنیتی برای هر وب سایتی که بازدید می کنید، کلمه‌های عبور قوی، منحصر به فرد و تصادفی ایجاد کنید. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -ترجمه های جهانی -ترجمه Bitwarden به 40 زبان وجود دارد و به لطف جامعه جهانی ما در حال رشد است. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -برنامه های کاربردی چند پلتفرمی -داده‌های حساس را در Bitwarden Vault خود از هر مرورگر، دستگاه تلفن همراه یا سیستم عامل دسکتاپ و غیره ایمن کنید و به اشتراک بگذارید. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - یک مدیریت کننده کلمه عبور رایگان برای تمامی دستگاه‌هایتان + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. همگام‌سازی و دسترسی به گاوصندوق خود از دستگاه های مختلف diff --git a/apps/browser/store/locales/fi/copy.resx b/apps/browser/store/locales/fi/copy.resx index 4440603239..42e914a13f 100644 --- a/apps/browser/store/locales/fi/copy.resx +++ b/apps/browser/store/locales/fi/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Ilmainen salasanahallinta + Bitwarden – Salasanahallinta - Turvallinen ja ilmainen salasanahallinta kaikille laitteillesi + Kotona, töissä tai reissussa, Bitwarden suojaa helposti kaikki salasanasi, avainkoodisi ja arkaluonteiset tietosi. - Bitwarden, Inc. on 8bit Solutions LLC:n emoyhtiö. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NIMENNYT PARHAAKSI SALASANOJEN HALLINNAKSI MM. THE VERGE, U.S. NEWS & WORLD REPORT JA CNET. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Hallinnoi, säilytä, suojaa ja jaa salasanoja rajattomalta laitemäärältä mistä tahansa. Bitwarden tarjoaa avoimeen lähdekoodin perustuvan salasanojen hallintaratkaisun kaikille, olitpa sitten kotona, töissä tai liikkeellä. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Luo usein käyttämillesi sivustoille automaattisesti vahvoja, yksilöllisiä ja satunnaisia salasanoja. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send -ominaisuudella lähetät tietoa nopeasti salattuna — tiedostoja ja tekstiä — suoraan kenelle tahansa. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Yritystoimintaan Bitwarden tarjoaa yrityksille Teams- ja Enterprise-tilaukset, jotta salasanojen jakaminen kollegoiden kesken on turvallista. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Miksi Bitwarden?: -Maailmanluokan salaus -Salasanat on suojattu tehokkaalla päästä päähän salauksella (AES-256 bit, suolattu hajautus ja PBKDF2 SHA-256), joten tietosi pysyvät turvassa ja yksityisinä. +More reasons to choose Bitwarden: -Sisäänrakennettu salasanageneraattori -Luo usein käyttämillesi sivustoille vahvoja, yksilöllisiä ja satunnaisia salasanoja. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Monikielinen -Bitwardenin sovelluksia on käännetty yli 40 kielelle ja määrä kasvaa jatkuvasti, kiitos kansainvälisen yhteisömme. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Alustariippumattomaton -Suojaa, käytä ja jaa Bitwarden-holvisi arkaluontoisia tietoja kaikilla selaimilla, mobiililaitteilla, pöytätietokoneilla ja muissa järjestelmissä. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Turvallinen ja ilmainen salasanahallinta kaikille laitteillesi + Kotona, töissä tai reissussa, Bitwarden suojaa helposti kaikki salasanasi, avainkoodisi ja arkaluonteiset tietosi. Synkronoi ja hallitse holviasi useilla laitteilla diff --git a/apps/browser/store/locales/fil/copy.resx b/apps/browser/store/locales/fil/copy.resx index 4947fa6996..0f68a90bfa 100644 --- a/apps/browser/store/locales/fil/copy.resx +++ b/apps/browser/store/locales/fil/copy.resx @@ -1,17 +1,17 @@  - @@ -118,17 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Libreng Password Manager + Bitwarden Password Manager - Isang ligtas at libreng password manager para sa lahat ng iyong mga aparato. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Si Bitwarden, Inc. ang parent company ng 8bit Solutions LLC. Tinawag na Pinakamahusay na Password Manager ng The Verge, U.S. News & World Report, CNET at iba pa. I-manage, i-store, i-secure at i-share ang walang limitasyong mga password sa walang limitasyong mga device mula sa kahit saan. Bitwarden nagbibigay ng mga open source na solusyon sa password management sa lahat, kahit sa bahay, sa trabaho o habang nasa daan. Lumikha ng mga matatas, natatanging, at mga random na password na naka-base sa mga pangangailangan ng seguridad para sa bawat website na madalas mong bisitahin. Ang Bitwarden Send ay nagpapadala ng maayos na naka-encrypt na impormasyon - mga file at plaintext - diretso sa sinuman. Ang Bitwarden ay nag-aalok ng mga Teams at Enterprise plans para sa m. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. + +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. + +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. + +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. + +Use Bitwarden to secure your workforce and share sensitive information with colleagues. + + +More reasons to choose Bitwarden: + +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. + +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. + +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Isang ligtas at libreng password manager para sa lahat ng iyong mga aparato. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. I-sync at i-access ang iyong kahadeyero mula sa maraming mga aparato diff --git a/apps/browser/store/locales/fr/copy.resx b/apps/browser/store/locales/fr/copy.resx index 9d311fe7cf..9927f885d3 100644 --- a/apps/browser/store/locales/fr/copy.resx +++ b/apps/browser/store/locales/fr/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Gestion des mots de passe + Bitwarden Password Manager - Un gestionnaire de mots de passe sécurisé et gratuit pour tous vos appareils + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. est la société mère de 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMMÉ MEILLEUR GESTIONNAIRE DE MOTS DE PASSE PAR THE VERGE, U.S. NEWS & WORLD REPORT, CNET, ET PLUS ENCORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gérez, stockez, sécurisez et partagez un nombre illimité de mots de passe sur un nombre illimité d'appareils, où que vous soyez. Bitwarden fournit des solutions de gestion de mots de passe open source à tout le monde, que ce soit chez soi, au travail ou en déplacement. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Générez des mots de passe robustes, uniques et aléatoires basés sur des exigences de sécurité pour chaque site web que vous fréquentez. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send transmet rapidement des informations chiffrées --- fichiers et texte --- directement à quiconque. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden propose des plans Teams et Enterprise pour les sociétés afin que vous puissiez partager des mots de passe en toute sécurité avec vos collègues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Pourquoi choisir Bitwarden : -Un chiffrement de classe internationale -Les mots de passe sont protégés par un cryptage avancé de bout en bout (AES-256 bit, hachage salé et PBKDF2 SHA-256) afin que vos données restent sécurisées et privées. +More reasons to choose Bitwarden: -Générateur de mots de passe intégré -Générez des mots de passe forts, uniques et aléatoires en fonction des exigences de sécurité pour chaque site web que vous fréquentez. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traductions internationales -Les traductions de Bitwarden existent dans 40 langues et ne cessent de croître, grâce à notre communauté globale. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Applications multiplateformes -Sécurisez et partagez des données sensibles dans votre coffre Bitwarden à partir de n'importe quel navigateur, appareil mobile ou système d'exploitation de bureau, et plus encore. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Un gestionnaire de mots de passe sécurisé et gratuit pour tous vos appareils + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synchroniser et accéder à votre coffre depuis plusieurs appareils diff --git a/apps/browser/store/locales/gl/copy.resx b/apps/browser/store/locales/gl/copy.resx index d812256fb7..0fdb224988 100644 --- a/apps/browser/store/locales/gl/copy.resx +++ b/apps/browser/store/locales/gl/copy.resx @@ -1,17 +1,17 @@  - @@ -118,47 +118,64 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Xestor de contrasinais gratuíto + Bitwarden Password Manager - Un xestor de contrasinais seguro e gratuíto para todos os teus dispositivos + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. é a empresa matriz de 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMEADO MELLOR ADMINISTRADOR DE CONTRASINAIS POR THE VERGE, Ou.S. NEWS & WORLD REPORT, CNET E MÁS. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Administre, almacene, protexa e comparta contrasinais ilimitados en dispositivos ilimitados desde calquera lugar. Bitwarden ofrece solucións de xestión de contrasinais de código aberto para todos, xa sexa en casa, no traballo ou en mentres estás de viaxe. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Xere contrasinais seguros, únicas e aleatorias en función dos requisitos de seguridade de cada sitio web que frecuenta. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send transmite rapidamente información cifrada --- arquivos e texto sen formato, directamente a calquera persoa. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden ofrece plans Teams e Enterprise para empresas para que poida compartir contrasinais de forma segura con colegas. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Por que elixir Bitwarden? -Cifrado de clase mundial -Os contrasinais están protexidas con cifrado avanzado de extremo a extremo (AES-256 bits, salted hashing e PBKDF2 XA-256) para que os seus datos permanezan seguros e privados. +More reasons to choose Bitwarden: -Xerador de contrasinais incorporado -Xere contrasinais fortes, únicas e aleatorias en función dos requisitos de seguridade de cada sitio web que frecuenta. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traducións Globais -As traducións de Bitwarden existen en 40 idiomas e están a crecer, grazas á nosa comunidade global. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplicacións multiplataforma -Protexa e comparta datos confidenciais dentro da súa Caixa Forte de Bitwarden desde calquera navegador, dispositivo móbil ou sistema operativo de escritorio, e máis. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Un xestor de contrasinais seguro e gratuíto para todos os teus dispositivos + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sincroniza e accede á túa caixa forte desde múltiples dispositivos - Xestiona todos os teus usuarios e contrasinais desde unha caixa forte segura + Xestiona todos os teus inicios de sesión e contrasinais desde unha caixa forte segura Autocompleta rapidamente os teus datos de acceso en calquera páxina web que visites diff --git a/apps/browser/store/locales/he/copy.resx b/apps/browser/store/locales/he/copy.resx index cd980970fc..7f366f0e93 100644 --- a/apps/browser/store/locales/he/copy.resx +++ b/apps/browser/store/locales/he/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – מנהל ססמאות חינמי + Bitwarden Password Manager - מנהל ססמאות חינמי ומאובטח עבור כל המכשירים שלך + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - מנהל סיסמאות חינמי ומאובטח עבור כל המכשירים שלך + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. סנכרון וגישה לכספת שלך ממגוון מכשירים diff --git a/apps/browser/store/locales/hi/copy.resx b/apps/browser/store/locales/hi/copy.resx index 8db837a3c3..1ea7314d52 100644 --- a/apps/browser/store/locales/hi/copy.resx +++ b/apps/browser/store/locales/hi/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - बिटवार्डन - मुक्त कूटशब्द प्रबंधक + Bitwarden Password Manager - आपके सभी उपकरणों के लिए एक सुरक्षित और नि: शुल्क कूटशब्द प्रबंधक + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - आपके सभी उपकरणों के लिए एक सुरक्षित और नि: शुल्क पासवर्ड प्रबंधक + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. अनेक उपकरणों से अपने तिजोरी सिंक और एक्सेस करें diff --git a/apps/browser/store/locales/hr/copy.resx b/apps/browser/store/locales/hr/copy.resx index 5ff2bcbe01..dff95b3796 100644 --- a/apps/browser/store/locales/hr/copy.resx +++ b/apps/browser/store/locales/hr/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - besplatni upravitelj lozinki + Bitwarden Password Manager - Siguran i besplatan upravitelj lozinki za sve vaše uređaje + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. je vlasnik tvrtke 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS & WORLD REPORT, CNET I DRUGI ODABRALI SU BITWARDEN NAJBOLJIM UPRAVITELJEM LOZINKI. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Upravljajte, spremajte, osigurajte i dijelite neograničen broj lozinki na neograničenom broju uređaja bilo gdje. Bitwarden omogućuje upravljanje lozinkama, bazirano na otvorenom kodu, svima, bilo kod kuće, na poslu ili u pokretu. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generirajte jake, jedinstvene i nasumične lozinke bazirane na sigurnosnim zahtjevima za svaku web stranicu koju često posjećujete. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send omoguzćuje jednostavno i brzo slanje šifriranih podataka --- datoteki ili teksta -- direktno, bilo kome. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden nudi Teams i Enterprise planove za tvrtke kako biste sigurno mogli dijeliti lozinke s kolegama na poslu. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Zašto odabrati Bitwarden? -Svjetski priznata enkripcija -Lozinke su zaštićene naprednim end-to-end šifriranjem (AES-256 bit, salted hashtag i PBKDF2 SHA-256) kako bi vaši osobni podaci ostali sigurni i samo vaši. +More reasons to choose Bitwarden: -Ugrađen generator lozinki -Generirajte jake, jedinstvene i nasumične lozinke bazirane na sigurnosnim zahtjevima za svako web mjesto koje često posjećujete. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Svjetski dostupan -Bitwarden je, zahvaljujući našoj globalnoj zajednici, dostupan na više od 40 jezika. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Podržani svi OS -Osigurajte i sigurno dijelite osjetljive podatke sadržane u vašem Bitwarden trezoru iz bilo kojeg preglednika, mobilnog uređaja ili stolnog računala s bilo kojim OS. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - Siguran i besplatan upravitelj lozinki za sve tvoje uređaje + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sinkroniziraj i pristupi svojem trezoru s više uređaja diff --git a/apps/browser/store/locales/hu/copy.resx b/apps/browser/store/locales/hu/copy.resx index 0b3761a8ad..3e6b8e42d4 100644 --- a/apps/browser/store/locales/hu/copy.resx +++ b/apps/browser/store/locales/hu/copy.resx @@ -1,17 +1,17 @@  - @@ -118,36 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Ingyenes jelszókezelő + Bitwarden Password Manager - Egy biztonságos és ingyenes jelszókezelő az összes eszközre. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - A Bitwarden, Inc. a 8bit Solutions LLC anyavállalata. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -A VERGE, A US NEWS & WORLD REPORT, a CNET ÉS MÁSOK LEGJOBB JELSZÓKEZELŐJE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Korlátlan számú jelszavak kezelése, tárolása, védelme és megosztása korlátlan eszközökön bárhonnan. A Bitwarden nyílt forráskódú jelszókezelési megoldásokat kínál mindenkinek, legyen az otthon, a munkahelyen vagy útközben. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Hozzunk létre erős, egyedi és véletlenszerű jelszavakat a biztonsági követelmények alapján minden webhelyre, amelyet gyakran látogatunk. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -A Bitwarden Send gyorsan továbbítja a titkosított információkat-fájlokat és egyszerű szöveget közvetlenül bárkinek. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -A Bitwarden csapatokat és vállalati terveket kínál a vállalatok számára, így biztonságosan megoszthatja jelszavait kollégáival. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Miért válasszuk a Bitwardent: -Világszínvonalú titkosítási jelszavak fejlett végpontok közötti titkosítással (AES-256 bit, titkosított hashtag és PBKDF2 SHA-256) védettek, így az adatok biztonságban és titokban maradnak. +More reasons to choose Bitwarden: -Beépített jelszógenerátor A biztonsági követelmények alapján erős, egyedi és véletlenszerű jelszavakat hozhat létre minden gyakran látogatott webhelyen. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globális fordítások +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -A Bitwarden fordítások 40 nyelven léteznek és globális közösségünknek köszönhetően egyre bővülnek. Többplatformos alkalmazások Biztonságos és megoszthatja az érzékeny adatokat a Bitwarden Széfben bármely böngészőből, mobileszközről vagy asztali operációs rendszerből stb. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - Egy biztonságos és ingyenes jelszókezelő az összes eszközre + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. A széf szinkronizálása és elérése több eszközön. diff --git a/apps/browser/store/locales/id/copy.resx b/apps/browser/store/locales/id/copy.resx index b52252a342..b0791fa3b1 100644 --- a/apps/browser/store/locales/id/copy.resx +++ b/apps/browser/store/locales/id/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Pengelola Sandi Gratis + Bitwarden Password Manager - Pengelola sandi yang aman dan gratis untuk semua perangkat Anda + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Pengelola sandi yang aman dan gratis untuk semua perangkat Anda + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sinkronkan dan akses brankas Anda dari beberapa perangkat diff --git a/apps/browser/store/locales/it/copy.resx b/apps/browser/store/locales/it/copy.resx index 56bf9a907c..bcbbe10512 100644 --- a/apps/browser/store/locales/it/copy.resx +++ b/apps/browser/store/locales/it/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Password Manager Gratis + Bitwarden Password Manager - Un password manager sicuro e gratis per tutti i tuoi dispositivi + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. è la società madre di 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMINATO MIGLIOR PASSWORD MANAGER DA THE VERGE, U.S. NEWS & WORLD REPORT, CNET, E ALTRO. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gestisci, archivia, proteggi, e condividi password illimitate su dispositivi illimitati da qualsiasi luogo. Bitwarden offre soluzioni di gestione delle password open-source a tutti, a casa, al lavoro, o in viaggio. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Genera password forti, uniche, e casuali in base ai requisiti di sicurezza per ogni sito web che frequenti. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send trasmette rapidamente informazioni crittate - via file e testo in chiaro - direttamente a chiunque. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offre piani Teams ed Enterprise per le aziende così puoi condividere le password in modo sicuro con i tuoi colleghi. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Perché Scegliere Bitwarden: -Crittografia Di Livello Mondiale -Le password sono protette con crittografia end-to-end avanzata (AES-256 bit, salted hashing, e PBKDF2 SHA-256) per tenere i tuoi dati al sicuro e privati. +More reasons to choose Bitwarden: -Generatore Di Password Integrato -Genera password forti, uniche e casuali in base ai requisiti di sicurezza per ogni sito web che frequenti. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traduzioni Globali -Le traduzioni di Bitwarden esistono in 40 lingue e sono in crescita grazie alla nostra comunità globale. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Applicazioni Multipiattaforma -Proteggi e condividi i dati sensibili all'interno della tua cassaforte di Bitwarden da qualsiasi browser, dispositivo mobile, o sistema operativo desktop, e altro. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Un password manager sicuro e gratis per tutti i tuoi dispositivi + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sincronizza e accedi alla tua cassaforte da più dispositivi diff --git a/apps/browser/store/locales/ja/copy.resx b/apps/browser/store/locales/ja/copy.resx index 13ce1bc4e9..67c479fcde 100644 --- a/apps/browser/store/locales/ja/copy.resx +++ b/apps/browser/store/locales/ja/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - 無料パスワードマネージャー + Bitwarden Password Manager - あらゆる端末で使える、安全な無料パスワードマネージャー + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc.は8bit Solutions LLC.の親会社です。 + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGEやU.S. NEWS、WORLD REPORT、CNETなどからベストパスワードマネージャーに選ばれました。 +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -端末や場所を問わずパスワードの管理・保存・保護・共有を無制限にできます。Bitwardenは自宅や職場、外出先でもパスワード管理をすべての人に提供し、プログラムコードは公開されています。 +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -よく利用するどのWebサイトでも、セキュリティ条件にそった強力でユニークなパスワードをランダムに生成することができます。 +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Sendは、暗号化した情報(ファイルや平文)をすぐに誰にでも直接送信することができます。 +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwardenは企業向けにTeamsとEnterpriseのプランを提供しており、パスワードを同僚と安全に共有することができます。 +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Bitwardenを選ぶ理由は? -・世界最高レベルの暗号化 -パスワードは高度なエンドツーエンド暗号化(AES-256 bit、salted hashing、PBKDF2 SHA-256)で保護されるので、データは安全に非公開で保たれます。 +More reasons to choose Bitwarden: -・パスワード生成機能 -よく利用するどのWebサイトでも、セキュリティ条件にそった強力でユニークなパスワードをランダムに生成することができます。 +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -・グローバルな翻訳 -Bitwardenは40ヶ国語に翻訳されており、グローバルなコミュニティのおかげで増え続けています。 +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -・クロスプラットフォームアプリケーション -あなたのBitwarden Vaultで、ブラウザ・モバイル機器・デスクトップOSなどの垣根を超えて、機密データを保護・共有することができます。 +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - あらゆる端末で使える、安全な無料パスワードマネージャー + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. 複数の端末で保管庫に同期&アクセス diff --git a/apps/browser/store/locales/ka/copy.resx b/apps/browser/store/locales/ka/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/ka/copy.resx +++ b/apps/browser/store/locales/ka/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/km/copy.resx b/apps/browser/store/locales/km/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/km/copy.resx +++ b/apps/browser/store/locales/km/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/kn/copy.resx b/apps/browser/store/locales/kn/copy.resx index 6928f557e4..f68f2c25da 100644 --- a/apps/browser/store/locales/kn/copy.resx +++ b/apps/browser/store/locales/kn/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - ಬಿಟ್ವರ್ಡ್ – ಉಚಿತ ಪಾಸ್ವರ್ಡ್ ನಿರ್ವಾಹಕ + Bitwarden Password Manager - ನಿಮ್ಮ ಎಲ್ಲಾ ಸಾಧನಗಳಿಗೆ ಸುರಕ್ಷಿತ ಮತ್ತು ಉಚಿತ ಪಾಸ್‌ವರ್ಡ್ ನಿರ್ವಾಹಕ + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - ಬಿಟ್ವಾರ್ಡೆನ್, ಇಂಕ್. 8 ಬಿಟ್ ಸೊಲ್ಯೂಷನ್ಸ್ ಎಲ್ಎಲ್ ಸಿ ಯ ಮೂಲ ಕಂಪನಿಯಾಗಿದೆ. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -ವರ್ಜ್, ಯು.ಎಸ್. ನ್ಯೂಸ್ & ವರ್ಲ್ಡ್ ರಿಪೋರ್ಟ್, ಸಿನೆಟ್ ಮತ್ತು ಹೆಚ್ಚಿನದರಿಂದ ಉತ್ತಮ ಪಾಸ್‌ವರ್ಡ್ ವ್ಯವಸ್ಥಾಪಕ ಎಂದು ಹೆಸರಿಸಲಾಗಿದೆ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -ಎಲ್ಲಿಂದಲಾದರೂ ಅನಿಯಮಿತ ಸಾಧನಗಳಲ್ಲಿ ಅನಿಯಮಿತ ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ನಿರ್ವಹಿಸಿ, ಸಂಗ್ರಹಿಸಿ, ಸುರಕ್ಷಿತಗೊಳಿಸಿ ಮತ್ತು ಹಂಚಿಕೊಳ್ಳಿ. ಮನೆಯಲ್ಲಿ, ಕೆಲಸದಲ್ಲಿ ಅಥವಾ ಪ್ರಯಾಣದಲ್ಲಿರಲಿ ಪ್ರತಿಯೊಬ್ಬರಿಗೂ ಬಿಟ್‌ವಾರ್ಡೆನ್ ಓಪನ್ ಸೋರ್ಸ್ ಪಾಸ್‌ವರ್ಡ್ ನಿರ್ವಹಣಾ ಪರಿಹಾರಗಳನ್ನು ನೀಡುತ್ತದೆ. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -ನೀವು ಆಗಾಗ್ಗೆ ಪ್ರತಿ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಸುರಕ್ಷತಾ ಅವಶ್ಯಕತೆಗಳನ್ನು ಆಧರಿಸಿ ಬಲವಾದ, ಅನನ್ಯ ಮತ್ತು ಯಾದೃಚ್ pass ಿಕ ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ರಚಿಸಿ. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -ಬಿಟ್‌ವಾರ್ಡೆನ್ ಕಳುಹಿಸಿ ಎನ್‌ಕ್ರಿಪ್ಟ್ ಮಾಡಿದ ಮಾಹಿತಿಯನ್ನು ತ್ವರಿತವಾಗಿ ರವಾನಿಸುತ್ತದೆ --- ಫೈಲ್‌ಗಳು ಮತ್ತು ಸರಳ ಪಠ್ಯ - ನೇರವಾಗಿ ಯಾರಿಗಾದರೂ. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -ಬಿಟ್‌ವಾರ್ಡೆನ್ ಕಂಪೆನಿಗಳಿಗೆ ತಂಡಗಳು ಮತ್ತು ಎಂಟರ್‌ಪ್ರೈಸ್ ಯೋಜನೆಗಳನ್ನು ನೀಡುತ್ತದೆ ಆದ್ದರಿಂದ ನೀವು ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ಸಹೋದ್ಯೋಗಿಗಳೊಂದಿಗೆ ಸುರಕ್ಷಿತವಾಗಿ ಹಂಚಿಕೊಳ್ಳಬಹುದು. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -ಬಿಟ್‌ವಾರ್ಡೆನ್ ಅನ್ನು ಏಕೆ ಆರಿಸಬೇಕು: -ವಿಶ್ವ ದರ್ಜೆಯ ಗೂ ry ಲಿಪೀಕರಣ -ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ಸುಧಾರಿತ ಎಂಡ್-ಟು-ಎಂಡ್ ಎನ್‌ಕ್ರಿಪ್ಶನ್ (ಎಇಎಸ್ -256 ಬಿಟ್, ಉಪ್ಪುಸಹಿತ ಹ್ಯಾಶ್‌ಟ್ಯಾಗ್ ಮತ್ತು ಪಿಬಿಕೆಡಿಎಫ್ 2 ಎಸ್‌ಎಚ್‌ಎ -256) ನೊಂದಿಗೆ ರಕ್ಷಿಸಲಾಗಿದೆ ಆದ್ದರಿಂದ ನಿಮ್ಮ ಡೇಟಾ ಸುರಕ್ಷಿತ ಮತ್ತು ಖಾಸಗಿಯಾಗಿರುತ್ತದೆ. +More reasons to choose Bitwarden: -ಅಂತರ್ನಿರ್ಮಿತ ಪಾಸ್ವರ್ಡ್ ಜನರೇಟರ್ -ನೀವು ಆಗಾಗ್ಗೆ ಪ್ರತಿ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಸುರಕ್ಷತಾ ಅವಶ್ಯಕತೆಗಳನ್ನು ಆಧರಿಸಿ ಬಲವಾದ, ಅನನ್ಯ ಮತ್ತು ಯಾದೃಚ್ pass ಿಕ ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ರಚಿಸಿ. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -ಜಾಗತಿಕ ಅನುವಾದಗಳು -ಬಿಟ್ವಾರ್ಡೆನ್ ಅನುವಾದಗಳು 40 ಭಾಷೆಗಳಲ್ಲಿ ಅಸ್ತಿತ್ವದಲ್ಲಿವೆ ಮತ್ತು ಬೆಳೆಯುತ್ತಿವೆ, ನಮ್ಮ ಜಾಗತಿಕ ಸಮುದಾಯಕ್ಕೆ ಧನ್ಯವಾದಗಳು. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -ಕ್ರಾಸ್ ಪ್ಲಾಟ್‌ಫಾರ್ಮ್ ಅಪ್ಲಿಕೇಶನ್‌ಗಳು -ಯಾವುದೇ ಬ್ರೌಸರ್, ಮೊಬೈಲ್ ಸಾಧನ, ಅಥವಾ ಡೆಸ್ಕ್‌ಟಾಪ್ ಓಎಸ್ ಮತ್ತು ಹೆಚ್ಚಿನವುಗಳಿಂದ ನಿಮ್ಮ ಬಿಟ್‌ವಾರ್ಡನ್ ವಾಲ್ಟ್‌ನಲ್ಲಿ ಸೂಕ್ಷ್ಮ ಡೇಟಾವನ್ನು ಸುರಕ್ಷಿತಗೊಳಿಸಿ ಮತ್ತು ಹಂಚಿಕೊಳ್ಳಿ. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - ನಿಮ್ಮ ಎಲ್ಲಾ ಸಾಧನಗಳಿಗೆ ಸುರಕ್ಷಿತ ಮತ್ತು ಉಚಿತ ಪಾಸ್‌ವರ್ಡ್ ನಿರ್ವಾಹಕ + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. ಅನೇಕ ಸಾಧನಗಳಿಂದ ನಿಮ್ಮ ವಾಲ್ಟ್ ಅನ್ನು ಸಿಂಕ್ ಮಾಡಿ ಮತ್ತು ಪ್ರವೇಶಿಸಿ diff --git a/apps/browser/store/locales/ko/copy.resx b/apps/browser/store/locales/ko/copy.resx index 0fb5dd713f..fdfb93ad6a 100644 --- a/apps/browser/store/locales/ko/copy.resx +++ b/apps/browser/store/locales/ko/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - 무료 비밀번호 관리자 + Bitwarden Password Manager - 당신의 모든 기기에서 사용할 수 있는, 안전한 무료 비밀번호 관리자 + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc.은 8bit Solutions LLC.의 모회사입니다. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -VERGE, U.S. NEWS, WORLD REPORT, CNET 등에서 최고의 비밀번호 관리자라고 평가했습니다! +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -관리하고, 보관하고, 보호하고, 어디에서든 어떤 기기에서나 무제한으로 비밀번호를 공유하세요. Bitwarden은 모두에게 오픈소스 비밀번호 관리 솔루션을 제공합니다. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -강하고, 독특하고, 랜덤한 비밀번호를 모든 웹사이트의 보안 요구사항에 따라 생성할 수 있습니다. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send는 빠르게 암호화된 파일과 텍스트를 모두에게 전송할 수 있습니다. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden은 회사들을 위해 팀과 기업 플랜을 제공해서 동료에게 안전하게 비밀번호를 공유할 수 있습니다. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Bitwarden을 선택하는 이유: -세계 최고의 암호화 -비밀번호는 고급 종단간 암호화 (AES-256 bit, salted hashtag, 그리고 PBKDF2 SHA-256)을 이용하여 보호되기 때문에 데이터를 안전하게 보관할 수 있습니다. +More reasons to choose Bitwarden: -내장 비밀번호 생성기 -강하고, 독특하고, 랜덤한 비밀번호를 모든 웹사이트의 보안 요구사항에 따라 생성할 수 있습니다. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -언어 지원 -Bitwarden 번역은 전 세계의 커뮤니티 덕분에 40개의 언어를 지원하고 더 성장하고 있습니다. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -크로스 플랫폼 애플리케이션 -Bitwarden 보관함에 있는 민감한 정보를 어떠한 브라우저, 모바일 기기, 데스크톱 OS 등을 이용하여 보호하고 공유하세요. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - 당신의 모든 기기에서 사용할 수 있는, 안전한 무료 비밀번호 관리자 + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. 여러 기기에서 보관함에 접근하고 동기화할 수 있습니다. diff --git a/apps/browser/store/locales/lt/copy.resx b/apps/browser/store/locales/lt/copy.resx index 92009c5c6d..d83c6ca99a 100644 --- a/apps/browser/store/locales/lt/copy.resx +++ b/apps/browser/store/locales/lt/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – nemokamas slaptažodžių tvarkyklė + Bitwarden Password Manager - Saugi ir nemokama slaptažodžių tvarkyklė visiems įrenginiams + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. yra patronuojančioji 8bit Solutions LLC įmonė. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -GERIAUSIU SLAPTAŽODŽIŲ TVARKYTOJU PRIPAŽINTAS THE VERGE, U.S. NEWS & WORLD REPORT, CNET IR KT. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Tvarkyk, laikyk, saugok ir bendrink neribotą skaičių slaptažodžių neribotuose įrenginiuose iš bet kurios vietos. Bitwarden teikia atvirojo kodo slaptažodžių valdymo sprendimus visiems – tiek namuose, tiek darbe, ar keliaujant. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generuok stiprius, unikalius ir atsitiktinius slaptažodžius pagal saugos reikalavimus kiekvienai lankomai svetainei. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send greitai perduoda užšifruotą informaciją – failus ir paprastą tekstą – tiesiogiai bet kam. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden siūlo komandoms ir verslui planus įmonėms, kad galėtum saugiai dalytis slaptažodžiais su kolegomis. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Kodėl rinktis Bitwarden: -Pasaulinės klasės šifravimas -Slaptažodžiai yra saugomi su pažangiu šifravimu nuo galo iki galo (AES-256 bitų, sūdytu šifravimu ir PBKDF2 SHA-256), todėl tavo duomenys išliks saugūs ir privatūs. +More reasons to choose Bitwarden: -Integruotas slaptažodžių generatorius -Generuok stiprius, unikalius ir atsitiktinius slaptažodžius pagal saugos reikalavimus kiekvienai dažnai lankomai svetainei. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Visuotiniai vertimai -Mūsų pasaulinės bendruomenės dėka Bitwarden vertimai egzistuoja 40 kalbose ir vis daugėja. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Įvairių platformų programos -Apsaugok ir bendrink neskelbtinus duomenis savo Bitwarden Vault iš bet kurios naršyklės, mobiliojo įrenginio ar darbalaukio OS ir kt. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Saugi ir nemokama slaptažodžių tvarkyklė visiems įrenginiams + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Pasiekite savo saugyklą iš kelių įrenginių diff --git a/apps/browser/store/locales/lv/copy.resx b/apps/browser/store/locales/lv/copy.resx index aec5e836c1..e64cc2eb3a 100644 --- a/apps/browser/store/locales/lv/copy.resx +++ b/apps/browser/store/locales/lv/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Bezmaksas Paroļu Pārvaldnieks + Bitwarden Password Manager - Drošs un bezmaksas paroļu pārvaldnieks priekš visām jūsu ierīcēm. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. ir 8bit Solutions LLC mātesuzņēmums. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS & WORLD REPORT, CNET UN CITI ATZINA PAR LABĀKO PAROĻU PĀRVALDNIEKU. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Pārvaldi, uzglabā, aizsargā un kopīgo neierobežotu skaitu paroļu neierobežotā skaitā ierīču no jebkuras vietas. Bitwarden piedāvā atvērtā koda paroļu pārvaldības risinājumus ikvienam - gan mājās, gan darbā, gan ceļā. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Ģenerē spēcīgas, unikālas un nejaušas paroles, pamatojoties uz drošības prasībām, katrai bieži apmeklētai vietnei. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send ātri pārsūta šifrētu informāciju - failus un atklātu tekstu - tieši jebkuram. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden piedāvā Teams un Enterprise plānus uzņēmumiem, lai tu varētu droši kopīgot paroles ar kolēģiem. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Kāpēc izvēlēties Bitwarden: -Pasaules klases šifrēšana -Paroles tiek aizsargātas ar modernu end-to-end šifrēšanu (AES-256 bitu, sālītu šifrēšanu un PBKDF2 SHA-256), lai tavi dati paliktu droši un privāti. +More reasons to choose Bitwarden: -Iebūvēts paroļu ģenerators -Ģenerē spēcīgas, unikālas un nejaušas paroles, pamatojoties uz drošības prasībām katrai bieži apmeklētai vietnei. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globālie tulkojumi -Bitwarden tulkojumi ir pieejami 40 valodās, un to skaits turpina pieaugt, pateicoties mūsu globālajai kopienai. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Starpplatformu lietojumprogrammas -Nodrošini un kopīgo sensitīvus datus savā Bitwarden Seifā no jebkuras pārlūkprogrammas, mobilās ierīces vai darbvirsmas operētājsistēmas un daudz ko citu. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Drošs un bezmaksas paroļu pārvaldnieks priekš visām jūsu ierīcēm. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sinhronizē un piekļūsti savai glabātavai no vairākām ierīcēm diff --git a/apps/browser/store/locales/ml/copy.resx b/apps/browser/store/locales/ml/copy.resx index cf9b631227..e22993d5b7 100644 --- a/apps/browser/store/locales/ml/copy.resx +++ b/apps/browser/store/locales/ml/copy.resx @@ -1,17 +1,17 @@  - @@ -118,27 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - സൗജന്യ പാസ്സ്‌വേഡ് മാനേജർ + Bitwarden Password Manager - നിങ്ങളുടെ എല്ലാ ഉപകരണങ്ങൾക്കും സുരക്ഷിതവും സൗജന്യവുമായ പാസ്‌വേഡ് മാനേജർ + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - നിങ്ങളുടെ എല്ലാ ലോഗിനുകളും പാസ്‌വേഡുകളും സംഭരിക്കുന്നതിനുള്ള ഏറ്റവും എളുപ്പവും സുരക്ഷിതവുമായ മാർഗ്ഗമാണ് Bitwarden, ഒപ്പം നിങ്ങളുടെ എല്ലാ ഉപകരണങ്ങളും തമ്മിൽ സമന്വയിപ്പിക്കുകയും ചെയ്യുന്നു. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -പാസ്‌വേഡ് മോഷണം ഗുരുതരമായ പ്രശ്‌നമാണ്. നിങ്ങൾ ഉപയോഗിക്കുന്ന വെബ്‌സൈറ്റുകളും അപ്ലിക്കേഷനുകളും എല്ലാ ദിവസവും ആക്രമണത്തിലാണ്. സുരക്ഷാ ലംഘനങ്ങൾ സംഭവിക്കുകയും നിങ്ങളുടെ പാസ്‌വേഡുകൾ മോഷ്‌ടിക്കപ്പെടുകയും ചെയ്യുന്നു. അപ്ലിക്കേഷനുകളിലും വെബ്‌സൈറ്റുകളിലും ഉടനീളം സമാന പാസ്‌വേഡുകൾ നിങ്ങൾ വീണ്ടും ഉപയോഗിക്കുമ്പോൾ ഹാക്കർമാർക്ക് നിങ്ങളുടെ ഇമെയിൽ, ബാങ്ക്, മറ്റ് പ്രധാനപ്പെട്ട അക്കൗണ്ടുകൾ എന്നിവ എളുപ്പത്തിൽ ആക്‌സസ്സുചെയ്യാനാകും. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -നിങ്ങളുടെ എല്ലാ ഉപകരണങ്ങളിലും സമന്വയിപ്പിക്കുന്ന ഒരു എൻ‌ക്രിപ്റ്റ് ചെയ്ത വാൾട്ടിൽ Bitwarden നിങ്ങളുടെ എല്ലാ ലോഗിനുകളും സംഭരിക്കുന്നു. നിങ്ങളുടെ ഉപകരണം വിടുന്നതിനുമുമ്പ് ഇത് പൂർണ്ണമായും എൻ‌ക്രിപ്റ്റ് ചെയ്‌തിരിക്കുന്നതിനാൽ, നിങ്ങളുടെ ഡാറ്റ നിങ്ങൾക്ക് മാത്രമേ ആക്‌സസ് ചെയ്യാൻ കഴിയൂ . Bitwarden ടീമിന് പോലും നിങ്ങളുടെ ഡാറ്റ വായിക്കാൻ കഴിയില്ല. നിങ്ങളുടെ ഡാറ്റ AES-256 ബിറ്റ് എൻ‌ക്രിപ്ഷൻ, സാൾട്ടിങ് ഹാഷിംഗ്, PBKDF2 SHA-256 എന്നിവ ഉപയോഗിച്ച് അടച്ചിരിക്കുന്നു. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -100% ഓപ്പൺ സോഴ്‌സ് സോഫ്റ്റ്വെയറാണ് Bitwarden . Bitwarden സോഴ്‌സ് കോഡ് GitHub- ൽ ഹോസ്റ്റുചെയ്‌തിരിക്കുന്നു, മാത്രമല്ല എല്ലാവർക്കും ഇത് അവലോകനം ചെയ്യാനും ഓഡിറ്റുചെയ്യാനും ബിറ്റ് വാർഡൻ കോഡ്ബേസിലേക്ക് സംഭാവന ചെയ്യാനും സ്വാതന്ത്ര്യമുണ്ട്. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. + +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. + +Use Bitwarden to secure your workforce and share sensitive information with colleagues. +More reasons to choose Bitwarden: +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. + +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - നിങ്ങളുടെ എല്ലാ ഉപകരണങ്ങൾക്കും സുരക്ഷിതവും സൗജന്യവുമായ പാസ്‌വേഡ് മാനേജർ. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. ഒന്നിലധികം ഉപകരണങ്ങളിൽ നിന്ന് നിങ്ങളുടെ വാൾട് സമന്വയിപ്പിച്ച് ആക്‌സസ്സുചെയ്യുക diff --git a/apps/browser/store/locales/mr/copy.resx b/apps/browser/store/locales/mr/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/mr/copy.resx +++ b/apps/browser/store/locales/mr/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/my/copy.resx b/apps/browser/store/locales/my/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/my/copy.resx +++ b/apps/browser/store/locales/my/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/nb/copy.resx b/apps/browser/store/locales/nb/copy.resx index 74a8558db6..26a09cc855 100644 --- a/apps/browser/store/locales/nb/copy.resx +++ b/apps/browser/store/locales/nb/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden — Fri passordbehandling + Bitwarden Password Manager - En sikker og fri passordbehandler for alle dine PCer og mobiler + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - En sikker og fri passordbehandler for alle dine PCer og mobiler + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synkroniser og få tilgang til ditt hvelv fra alle dine enheter diff --git a/apps/browser/store/locales/ne/copy.resx b/apps/browser/store/locales/ne/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/ne/copy.resx +++ b/apps/browser/store/locales/ne/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/nl/copy.resx b/apps/browser/store/locales/nl/copy.resx index e0779ba777..44dd02b439 100644 --- a/apps/browser/store/locales/nl/copy.resx +++ b/apps/browser/store/locales/nl/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Gratis wachtwoordbeheer + Bitwarden Password Manager - Een veilige en gratis oplossing voor wachtwoordbeheer voor al je apparaten + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is het moederbedrijf van 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -BESTE WACHTWOORDBEHEERDER VOLGENS THE VERGE, U.S. NEWS & WORLD REPORT, CNET EN ANDEREN. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Beheer, bewaar, beveilig en deel een onbeperkt aantal wachtwoorden op een onbeperkt aantal apparaten, waar je ook bent. Bitwarden levert open source wachtwoordbeheeroplossingen voor iedereen, of dat nu thuis, op het werk of onderweg is. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Genereer sterke, unieke en willekeurige wachtwoorden op basis van beveiligingsvereisten voor elke website die je bezoekt. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send verzendt snel versleutelde informatie --- bestanden en platte tekst -- rechtstreeks naar iedereen. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden biedt Teams- en Enterprise-abonnementen voor bedrijven, zodat je veilig wachtwoorden kunt delen met collega's. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Waarom Bitwarden: -Versleuteling van wereldklasse -Wachtwoorden worden beschermd met geavanceerde end-to-end-codering (AES-256 bit, salted hashtag en PBKDF2 SHA-256) zodat jouw gegevens veilig en privé blijven. +More reasons to choose Bitwarden: -Ingebouwde wachtwoordgenerator -Genereer sterke, unieke en willekeurige wachtwoorden op basis van beveiligingsvereisten voor elke website die je bezoekt. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Wereldwijde vertalingen -Bitwarden-vertalingen bestaan ​​in 40 talen en groeien dankzij onze wereldwijde community. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Platformoverschrijdende toepassingen -Beveilig en deel gevoelige gegevens binnen uw Bitwarden Vault vanuit elke browser, mobiel apparaat of desktop-besturingssysteem, en meer. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - Een veilige en gratis oplossing voor wachtwoordbeheer voor al uw apparaten + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synchroniseer en gebruik je kluis op meerdere apparaten diff --git a/apps/browser/store/locales/nn/copy.resx b/apps/browser/store/locales/nn/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/nn/copy.resx +++ b/apps/browser/store/locales/nn/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/or/copy.resx b/apps/browser/store/locales/or/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/or/copy.resx +++ b/apps/browser/store/locales/or/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/pl/copy.resx b/apps/browser/store/locales/pl/copy.resx index 5b3941cb7e..60709c7d4d 100644 --- a/apps/browser/store/locales/pl/copy.resx +++ b/apps/browser/store/locales/pl/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - darmowy menedżer haseł + Bitwarden Password Manager - Bezpieczny i darmowy menedżer haseł dla wszystkich Twoich urządzeń + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. jest macierzystą firmą 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAZWANY NAJLEPSZYM MENEDŻEREM HASEŁ PRZEZ THE VERGE, US NEWS & WORLD REPORT, CNET I WIĘCEJ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Zarządzaj, przechowuj, zabezpieczaj i udostępniaj nieograniczoną liczbę haseł na nieograniczonej liczbie urządzeń z każdego miejsca. Bitwarden dostarcza rozwiązania do zarządzania hasłami z otwartym kodem źródłowym każdemu, niezależnie od tego, czy jest w domu, w pracy, czy w podróży. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generuj silne, unikalne i losowe hasła w oparciu o wymagania bezpieczeństwa dla każdej odwiedzanej strony. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Funkcja Bitwarden Send szybko przesyła zaszyfrowane informacje --- pliki i zwykły tekst -- bezpośrednio do każdego. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden oferuje plany dla zespołów i firm, dzięki czemu możesz bezpiecznie udostępniać hasła współpracownikom. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Dlaczego warto wybrać Bitwarden: -Szyfrowanie światowej klasy -Hasła są chronione za pomocą zaawansowanego szyfrowania typu end-to-end (AES-256 bitów, dodatkowy ciąg zaburzający i PBKDF2 SHA-256), dzięki czemu Twoje dane pozostają bezpieczne i prywatne. +More reasons to choose Bitwarden: -Wbudowany generator haseł -Generuj silne, unikalne i losowe hasła w oparciu o wymagania bezpieczeństwa dla każdej odwiedzanej strony. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Przetłumaczone aplikacje -Tłumaczenia Bitwarden są dostępne w 40 językach i rosną dzięki naszej globalnej społeczności. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplikacje wieloplatformowe -Zabezpiecz i udostępniaj poufne dane w swoim sejfie Bitwarden z dowolnej przeglądarki, urządzenia mobilnego, systemu operacyjnego i nie tylko. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - Bezpieczny i darmowy menedżer haseł dla wszystkich Twoich urządzeń + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synchronizacja i dostęp do sejfu z różnych urządzeń diff --git a/apps/browser/store/locales/pt_BR/copy.resx b/apps/browser/store/locales/pt_BR/copy.resx index 48111fa814..8b99c436d0 100644 --- a/apps/browser/store/locales/pt_BR/copy.resx +++ b/apps/browser/store/locales/pt_BR/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Gerenciador de Senhas Gratuito + Bitwarden Password Manager - Um gerenciador de senhas gratuito e seguro para todos os seus dispositivos + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. é a empresa matriz da 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMEADA MELHOR GERENCIADORA DE SENHAS PELA VERGE, U.S. NEWS & WORLD REPORT, CNET, E MUITO MAIS. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gerenciar, armazenar, proteger e compartilhar senhas ilimitadas através de dispositivos ilimitados de qualquer lugar. Bitwarden fornece soluções de gerenciamento de senhas de código aberto para todos, seja em casa, no trabalho ou em viagem. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Gere senhas fortes, únicas e aleatórias com base nos requisitos de segurança para cada site que você frequenta. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -A Bitwarden Send transmite rapidamente informações criptografadas --- arquivos e texto em formato de placa -- diretamente para qualquer pessoa. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden oferece equipes e planos empresariais para empresas para que você possa compartilhar senhas com colegas com segurança. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Por que escolher Bitwarden: -Criptografia de Classe Mundial -As senhas são protegidas com criptografia avançada de ponta a ponta (AES-256 bit, salted hashing e PBKDF2 SHA-256) para que seus dados permaneçam seguros e privados. +More reasons to choose Bitwarden: -Gerador de senhas embutido -Gerar senhas fortes, únicas e aleatórias com base nos requisitos de segurança para cada site que você freqüenta. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traduções globais -As traduções Bitwarden existem em 40 idiomas e estão crescendo, graças à nossa comunidade global. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplicações multiplataforma -Proteja e compartilhe dados sensíveis dentro de seu Bitwarden Vault a partir de qualquer navegador, dispositivo móvel ou SO desktop, e muito mais. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Um gerenciador de senhas gratuito e seguro para todos os seus dispositivos + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sincronize e acesse o seu cofre através de múltiplos dispositivos diff --git a/apps/browser/store/locales/pt_PT/copy.resx b/apps/browser/store/locales/pt_PT/copy.resx index 845a94a3ca..d310629612 100644 --- a/apps/browser/store/locales/pt_PT/copy.resx +++ b/apps/browser/store/locales/pt_PT/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Gestor de Palavras-passe Gratuito + Bitwarden Password Manager - Um gestor de palavras-passe seguro e gratuito para todos os seus dispositivos + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - A Bitwarden, Inc. é a empresa-mãe da 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMEADO O MELHOR GESTOR DE PALAVRAS-PASSE PELO THE VERGE, U.S. NEWS & WORLD REPORT, CNET E MUITO MAIS. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gerir, armazenar, proteger e partilhar palavras-passe ilimitadas em dispositivos ilimitados a partir de qualquer lugar. O Bitwarden fornece soluções de gestão de palavras-passe de código aberto para todos, seja em casa, no trabalho ou onde estiver. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Gera palavras-passe fortes, únicas e aleatórias com base em requisitos de segurança para todos os sites que frequenta. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -O Bitwarden Send transmite rapidamente informações encriptadas - ficheiros e texto simples - diretamente a qualquer pessoa. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -O Bitwarden oferece os planos Equipas e Empresarial destinados a empresas, para que possa partilhar de forma segura as palavras-passe com os seus colegas. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Razões para escolher o Bitwarden: -Encriptação de classe mundial -As palavras-passe são protegidas com encriptação avançada de ponta a ponta (AES-256 bit, salted hashtag e PBKDF2 SHA-256) para que os seus dados permaneçam seguros e privados. +More reasons to choose Bitwarden: -Gerador de palavras-passe incorporado -Gera palavras-passe fortes, únicas e aleatórias com base nos requisitos de segurança para todos os sites que frequenta. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traduções globais -O Bitwarden está traduzido em 40 idiomas e está a crescer, graças à nossa comunidade global. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplicações multiplataforma -Proteja e partilhe dados confidenciais no seu cofre Bitwarden a partir de qualquer navegador, dispositivo móvel ou sistema operativo de computador, e muito mais. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Um gestor de palavras-passe seguro e gratuito para todos os seus dispositivos + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sincronize e aceda ao seu cofre através de vários dispositivos diff --git a/apps/browser/store/locales/ro/copy.resx b/apps/browser/store/locales/ro/copy.resx index 0e12b289af..7b0070fad2 100644 --- a/apps/browser/store/locales/ro/copy.resx +++ b/apps/browser/store/locales/ro/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Manager de parole gratuit + Bitwarden Password Manager - Un manager de parole sigur și gratuit pentru toate dispozitivele dvs. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. este compania mamă a 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NUMIT CEL MAI BUN MANAGER DE PAROLE DE CĂTRE THE VERGE, U.S. NEWS & WORLD REPORT, CNET ȘI MULȚI ALȚII. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gestionați, stocați, securizați și partajați un număr nelimitat de parole pe un număr nelimitat de dispozitive, de oriunde. Bitwarden oferă soluții open source de gestionare a parolelor pentru toată lumea, fie că se află acasă, la serviciu sau în mișcare. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generați parole puternice, unice și aleatorii, bazate pe cerințe de securitate pentru fiecare site web pe care îl frecventați. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send transmite rapid informații criptate --- fișiere și text simple -- direct către oricine. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden oferă planuri Teams și Enterprise pentru companii, astfel încât să puteți partaja în siguranță parolele cu colegii. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -De ce să alegeți Bitwarden: -Criptare de clasă mondială -Parolele sunt protejate cu criptare avansată end-to-end (AES-256 bit, salted hashing și PBKDF2 SHA-256), astfel încât datele dvs. să rămână sigure și private. +More reasons to choose Bitwarden: -Generator de parole încorporat -Generați parole puternice, unice și aleatorii, bazate pe cerințele de securitate pentru fiecare site web pe care îl frecventați. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traduceri la nivel mondial -Bitwarden este deja tradus în 40 de limbi și numărul lor crește, datorită comunității noastre mondiale. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplicații multi-platformă -Protejați și partajați date sensibile în seiful Bitwarden de pe orice browser, dispozitiv mobil sau sistem de operare desktop și multe altele. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Un manager de parole sigur și gratuit, pentru toate dispozitivele dvs. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sincronizează și accesează seiful dvs. de pe multiple dispozitive diff --git a/apps/browser/store/locales/ru/copy.resx b/apps/browser/store/locales/ru/copy.resx index 4e48ecbc88..212a899f76 100644 --- a/apps/browser/store/locales/ru/copy.resx +++ b/apps/browser/store/locales/ru/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – бесплатный менеджер паролей + Bitwarden Password Manager - Защищенный и бесплатный менеджер паролей для всех ваших устройств + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. является материнской компанией 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -НАЗВАН ЛУЧШИМ ДИСПЕТЧЕРОМ ПАРОЛЕЙ VERGE, US NEWS & WORLD REPORT, CNET И МНОГИМИ ДРУГИМИ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Управляйте, храните, защищайте и делитесь неограниченным количеством паролей на неограниченном количестве устройств из любого места. Bitwarden предоставляет решения с открытым исходным кодом по управлению паролями для всех, дома, на работе или в дороге. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Создавайте надежные, уникальные и случайные пароли на основе требований безопасности для каждого посещаемого вами сайта. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send быстро передает зашифрованную информацию - файлы и простой текст - напрямую кому угодно. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden предлагает для компаний планы Teams и Enterprise, чтобы вы могли безопасно делиться паролями с коллегами. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Почему выбирают Bitwarden: -Шифрование мирового класса -Пароли защищены передовым сквозным шифрованием (AES-256 bit, соленый хэштег и PBKDF2 SHA-256), поэтому ваши данные остаются в безопасности и конфиденциальности. +More reasons to choose Bitwarden: -Встроенный генератор паролей -Создавайте надежные, уникальные и случайные пароли на основе требований безопасности для каждого посещаемого вами сайта. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. - Глобальные переводы - Переводы Bitwarden существуют на 40 языках и постоянно растут благодаря нашему глобальному сообществу. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. - Кросс-платформенные приложения - Защищайте и делитесь конфиденциальными данными в вашем Bitwarden Vault из любого браузера, мобильного устройства, настольной ОС и т. д. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - Защищенный и бесплатный менеджер паролей для всех ваших устройств + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Синхронизация и доступ к хранилищу с нескольких устройств diff --git a/apps/browser/store/locales/si/copy.resx b/apps/browser/store/locales/si/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/si/copy.resx +++ b/apps/browser/store/locales/si/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/sk/copy.resx b/apps/browser/store/locales/sk/copy.resx index ba2a2a5a07..de7fa7dee3 100644 --- a/apps/browser/store/locales/sk/copy.resx +++ b/apps/browser/store/locales/sk/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Bezplatný správca hesiel + Bitwarden Password Manager - Bezpečný a bezplatný správca hesiel pre všetky vaše zariadenia + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. je materská spoločnosť spoločnosti 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -OHODNOTENÝ AKO NAJLEPŠÍ SPRÁVCA HESIEL V THE VERGE, U.S. NEWS & WORLD REPORT, CNET A ĎALŠÍMI. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Spravujte, ukladajte, zabezpečte a zdieľajte neobmedzený počet hesiel naprieč neobmedzeným počtom zariadení odkiaľkoľvek. Bitwarden ponúka open source riešenie na správu hesiel komukoľvek, kdekoľvek doma, v práci alebo na ceste. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Vygenerujte si silné, unikátne a náhodné heslá podľa bezpečnostných požiadaviek na každej stránke, ktorú navštevujete. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send rýchlo prenesie šifrované informácie -- súbory a text -- priamo komukoľvek. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden ponúka Teams a Enterprise paušály pre firmy, aby ste mohli bezpečne zdieľať hesla s kolegami. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Prečo si vybrať Bitwarden: -Svetová trieda v šifrovaní -Heslá sú chránené pokročilým end-to-end šifrovaním (AES-256 bit, salted hash a PBKDF2 SHA-256), takže Vaše dáta zostanú bezpečné a súkromné. +More reasons to choose Bitwarden: -Vstavaný generátor hesiel -Vygenerujte si silné, unikátne a náhodné heslá podľa bezpečnostných požiadaviek na každej stránke, ktorú navštevujete. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Svetová lokalizácia -Vďaka našej globálnej komunite má Bitwarden neustále rastúcu lokalizáciu už do 40 jazykov. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplikácie pre rôzne platformy -Zabezpečte a zdieľajte súkromné dáta prostredníctvom Bitwarden trezora z ktoréhokoľvek prehliadača, mobilného zariadenia, alebo stolného počítača a ďalších. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Bezpečný a bezplatný správca hesiel pre všetky vaše zariadenia + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synchronizujte a pristupujte k vášmu trezoru z viacerých zariadení diff --git a/apps/browser/store/locales/sl/copy.resx b/apps/browser/store/locales/sl/copy.resx index 83288e3872..80886de48a 100644 --- a/apps/browser/store/locales/sl/copy.resx +++ b/apps/browser/store/locales/sl/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - brezplačni upravljalnik gesel + Bitwarden Password Manager - Varen in brezplačen upravljalnik gesel za vse vaše naprave + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. je matično podjetje podjetja 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAJBOŠJI UPRAVLJALNIK GESEL PO MNEJU THE VERGE, U.S. NEWS & WORLD REPORT, CNET IN DRUGIH. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Upravljajte, shranjujte, varujte in delite neomejeno število gesel na neomejenem številu naprav, kjerkoli. Bitwarden ponuja odprtokodne rešitve za upravljanje gesel vsem, tako doma kot v službi ali na poti. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Ustvarite močna, edinstvena in naključna gesla, skladna z varnostnimi zahtevami za vsako spletno mesto, ki ga obiščete. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Z Bitwarden Send hitro prenesite šifrirane informacije --- datoteke in navadno besedilo -- neposredno komurkoli. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden ponuja storitvi za organizacije Teams in Enterprise, s katerima lahko gesla varno delite s sodelavci. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Zakaj izbrati Bitwarden: -Vrhunsko šifriranje -Gesla so zaščitena z naprednim šifriranjem (AES-256, soljene hash-vrednosti in PBKDF2 SHA-256), tako da vaši podatki ostanejo varni in zasebni. +More reasons to choose Bitwarden: -Vgrajeni generator gesel -Ustvarite močna, edinstvena in naključna gesla v skladu z varnostnimi zahtevami za vsako spletno mesto, ki ga obiščete. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Prevodi za ves svet -Bitwarden je preveden že v 40 jezikov, naša globalna skupnost pa ves čas posodabljan in ustvarja nove prevede. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Deluje na vseh platformah -Varujte in delite svoje občutljive podatke znotraj vašega Bitwarden trezorja v katerem koli brskalniku, mobilni napravi, namiznem računalniku in drugje. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - Varen in brezplačen upravljalnik gesel za vse vaše naprave + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sinhronizirajte svoj trezor gesel in dostopajte do njega z več naprav diff --git a/apps/browser/store/locales/sr/copy.resx b/apps/browser/store/locales/sr/copy.resx index 9bfe799035..9c34d5812a 100644 --- a/apps/browser/store/locales/sr/copy.resx +++ b/apps/browser/store/locales/sr/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Бесплатни Менаџер Лозинке + Bitwarden Password Manager - Сигурни и бесплатни менаџер лозинке за сва Ваша уређаја + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. је матична компанија фирме 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -Именован као најбољи управљач лозинкама од стране новинских сајтова као што су THE VERGE, U.S. NEWS & WORLD REPORT, CNET, и других. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Управљајте, чувајте, обезбедите, и поделите неограничен број лозинки са неограниченог броја уређаја где год да се налазите. Bitwarden свима доноси решења за управљање лозинкама која су отвореног кода, било да сте код куће, на послу, или на путу. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Генеришите јаке, јединствене, и насумичне лозинке у зависности од безбедносних захтева за сваки сајт који често посећујете. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send брзо преноси шифроване информације--- датотеке и обичан текст-- директно и свима. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden нуди планове за компаније и предузећа како бисте могли безбедно да делите лозинке са вашим колегама. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Зашто изабрати Bitwarden: -Шифровање светске класе -Лозинке су заштићене напредним шифровањем од једног до другог краја (AES-256 bit, salted hashing, и PBKDF2 SHA-256) како би ваши подаци остали безбедни и приватни. +More reasons to choose Bitwarden: -Уграђен генератор лозинки -Генеришите јаке, јединствене, и насумичне лозинке у зависности од безбедносних захтева за сваки сајт који често посећујете. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Глобално преведен -Bitwarden преводи постоје за 40 језика и стално се унапређују, захваљујући нашој глобалној заједници. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Вишеплатформне апликације -Обезбедите и поделите осетљиве податке у вашем Bitwarden сефу из било ког претраживача, мобилног уређаја, или desktop оперативног система, и других. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Сигурни и бесплатни менаџер лозинке за сва Ваша уређаја + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Синхронизујте и приступите сефу са више уређаја diff --git a/apps/browser/store/locales/sv/copy.resx b/apps/browser/store/locales/sv/copy.resx index 8b3cb2a402..6406ab013e 100644 --- a/apps/browser/store/locales/sv/copy.resx +++ b/apps/browser/store/locales/sv/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Gratis lösenordshanterare + Bitwarden Password Manager - En säker och gratis lösenordshanterare för alla dina enheter + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. är moderbolag till 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -UTNÄMND TILL DEN BÄSTA LÖSENORDSHANTERAREN AV THE VERGE, U.S. NEWS & WORLD REPORT, CNET MED FLERA. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Hantera, lagra, säkra och dela ett obegränsat antal lösenord mellan ett obegränsat antal enheter var som helst ifrån. Bitwarden levererar lösningar för lösenordshantering med öppen källkod till alla, vare sig det är hemma, på jobbet eller på språng. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generera starka, unika och slumpmässiga lösenord baserat på säkerhetskrav för varje webbplats du besöker. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send överför snabbt krypterad information --- filer och klartext -- direkt till vem som helst. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden erbjuder abonnemang för team och företag så att du säkert kan dela lösenord med kollegor. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Varför välja Bitwarden: -Kryptering i världsklass -Lösenord skyddas med avancerad end-to-end-kryptering (AES-256 bitar, saltad hashtag och PBKDF2 SHA-256) så att dina data förblir säkra och privata. +More reasons to choose Bitwarden: -Inbyggd lösenordsgenerator -Generera starka, unika och slumpmässiga lösenord baserat på säkerhetskrav för varje webbplats du besöker. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globala översättningar -Översättningar av Bitwarden finns på 40 språk och antalet växer tack vare vår globala gemenskap. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Plattformsoberoende program -Säkra och dela känsliga data i ditt Bitwardenvalv från alla webbläsare, mobiler och datorer. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - En säker och gratis lösenordshanterare för alla dina enheter + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synkronisera och kom åt ditt valv från flera enheter diff --git a/apps/browser/store/locales/te/copy.resx b/apps/browser/store/locales/te/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/te/copy.resx +++ b/apps/browser/store/locales/te/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/th/copy.resx b/apps/browser/store/locales/th/copy.resx index 9c8965b01f..f784b1884b 100644 --- a/apps/browser/store/locales/th/copy.resx +++ b/apps/browser/store/locales/th/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – โปรแกรมจัดการรหัสผ่านฟรี + Bitwarden Password Manager - โปรแกรมจัดการรหัสผ่านที่ปลอดภัยและฟรี สำหรับอุปกรณ์ทั้งหมดของคุณ + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. เป็นบริษัทแม่ของ 8bit Solutions LLC + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -ได้รับการระบุชื่อเป็น โปรแกรมจัดการรหัสผ่านที่ดีที่สุด โดย The Verge, U.S. News & World Report, CNET, และที่อื่นๆ +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -สามารถจัดการ จัดเก็บ ปกป้อง และแชร์รหัสผ่านไม่จำกัดจำนวนระหว่างอุปกรณ์ต่างๆ โดยไม่จำกัดจำนวนจากที่ไหนก็ได้ Bitwarden เสนอโซลูชันจัดการรหัสผ่านโอเพนซอร์สให้กับทุกคน ไม่ว่าจะอยู่ที่บ้าน ที่ทำงาน หรือนอกสถานที่ +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -สามารถส่มสร้างรหัสผ่านที่ปลอดภัยและไม่ซ้ำกัน ตามเงื่อนไขความปลอดภัยที่กำหนดได้ สำหรับเว็บไซต์ทุกแห่งที่คุณใช้งานบ่อย +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send สามารถส่งข้อมูลที่ถูกเข้ารหัส --- ไฟล์ หรือ ข้อความ -- ตรงไปยังใครก็ได้ได้อย่างรวดเร็ว +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden มีแผนแบบ Teams และ Enterprise สำหรับบริษัทต่างๆ ซึางคุณสามารถแชร์รหัสผ่านกับเพื่อนร่วมงานได้อย่างปลอดภัย +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -ทำไมควรเลือก Bitwarden: -การเข้ารหัสมาตรฐานโลก -รหัสผ่านจะได้รับการปกป้องด้วยการเข้ารหัสชั้นสูง (AES-256 บิต, salted hashtag, และ PBKDF2 SHA-256) แบบต้นทางถึงปลายทาง เพื่อให้ข้อมูลของคุณปลอดภัยและเป็นส่วนตัว +More reasons to choose Bitwarden: -มีตัวช่วยส่มสร้างรหัสผ่าน -สามารถสุ่มสร้างรหัสผ่านที่ปลอดภัยและไม่ซ้ำกัน ตามเงื่อนไขความปลอดภัยที่กำหนดได้ สำหรับเว็บไซต์ทุกแห่งที่คุณใช้งานบ่อย +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -แปลเป็นภาษาต่างๆ ทั่วโลก -Bitwarden ได้รับการแปลเป็นภาษาต่างๆ กว่า 40 ภาษา และกำลังเพิ่มขึ้นเรื่อยๆ ด้วยความสนับสนุนจากชุมชนผู้ใช้งานทั่วโลก +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -แอปพลิเคชันข้ามแพลตฟอร์ม -ปกป้องและแชร์ข้อมูลอ่อนไหวในตู้เซฟ Bitwarden จากเว็บเบราว์เซอร์ อุปกรณ์มือถือ หรือเดสท็อป หรือช่องทางอื่นๆ +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - โปรแกรมจัดการรหัสผ่านที่ปลอดภัยและฟรี สำหรับอุปกรณ์ทั้งหมดของคุณ + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. ซิงค์และเข้าถึงตู้นิรภัยของคุณจากหลายอุปกรณ์ diff --git a/apps/browser/store/locales/tr/copy.resx b/apps/browser/store/locales/tr/copy.resx index 1fc3e2a34b..539aad3aee 100644 --- a/apps/browser/store/locales/tr/copy.resx +++ b/apps/browser/store/locales/tr/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Ücretsiz Parola Yöneticisi + Bitwarden Password Manager - Tüm aygıtlarınız için güvenli ve ücretsiz bir parola yöneticisi + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc., 8bit Solutions LLC’nin ana şirketidir. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS & WORLD REPORT, CNET VE BİRÇOK MEDYA KURULUŞUNA GÖRE EN İYİ PAROLA YÖNETİCİSİ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Sınırsız sayıda parolayı istediğiniz kadar cihazda yönetin, saklayın, koruyun ve paylaşın. Bitwarden; herkesin evde, işte veya yolda kullanabileceği açık kaynaklı parola yönetim çözümleri sunuyor. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Sık kullandığınız web siteleri için güvenlik gereksinimlerinize uygun, güçlü, benzersiz ve rastgele parolalar oluşturabilirsiniz. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send, şifrelenmiş bilgileri (dosyalar ve düz metinler) herkese hızlı bir şekilde iletmenizi sağlıyor. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden, parolaları iş arkadaşlarınızla güvenli bir şekilde paylaşabilmeniz için şirketlere yönelik Teams ve Enterprise paketleri de sunuyor. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Neden Bitwarden? -Üst düzey şifreleme -Parolalarınız gelişmiş uçtan uca şifreleme (AES-256 bit, salted hashing ve PBKDF2 SHA-256) ile korunuyor, böylece verileriniz güvende ve gizli kalıyor. +More reasons to choose Bitwarden: -Dahili parola oluşturucu -Sık kullandığınız web siteleri için güvenlik gereksinimlerinize uygun, güçlü, benzersiz ve rastgele parolalar oluşturabilirsiniz. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Çeviriler -Bitwarden 40 dilde kullanılabiliyor ve gönüllü topluluğumuz sayesinde çeviri sayısı giderek artıyor. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Her platformla uyumlu uygulamalar -Bitwarden kasanızdaki hassas verilere her tarayıcıdan, mobil cihazdan veya masaüstü işletim sisteminden ulaşabilir ve onları paylaşabilirsiniz. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Tüm cihazarınız için güvenli ve ücretsiz bir parola yöneticisi + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Hesabınızı senkronize ederek kasanıza tüm cihazlarınızdan ulaşın diff --git a/apps/browser/store/locales/uk/copy.resx b/apps/browser/store/locales/uk/copy.resx index d59cd7f103..5a7de18363 100644 --- a/apps/browser/store/locales/uk/copy.resx +++ b/apps/browser/store/locales/uk/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Безплатний менеджер паролів + Bitwarden Password Manager - Захищений, безплатний менеджер паролів для всіх ваших пристроїв + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - 8bit Solutions LLC є дочірньою компанією Bitwarden, Inc. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -НАЙКРАЩИЙ МЕНЕДЖЕР ПАРОЛІВ ЗА ВЕРСІЄЮ THE VERGE, U.S. NEWS & WORLD REPORT, CNET, А ТАКОЖ ІНШИХ ВИДАНЬ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Зберігайте, захищайте, керуйте і надавайте доступ до паролів на різних пристроях де завгодно. Bitwarden пропонує рішення для керування паролями на основі відкритого програмного коду особистим та корпоративним користувачам на всіх пристроях. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Генеруйте надійні, випадкові та унікальні паролі, які відповідають вимогам безпеки, для кожного вебсайту та сервісу. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Швидко відправляйте будь-кому зашифровану інформацію, як-от файли чи звичайний текст, за допомогою функції Bitwarden Send. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden пропонує командні та корпоративні тарифні плани для компаній, щоб ви могли безпечно обмінюватися паролями з колегами. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Чому варто обрати Bitwarden: -Всесвітньо визнані стандарти шифрування -Паролі захищаються з використанням розширеного наскрізного шифрування (AES-256 bit, хешування з сіллю та PBKDF2 SHA-256), тому ваші дані завжди захищені та приватні. +More reasons to choose Bitwarden: -Вбудований генератор паролів -Генеруйте надійні, випадкові та унікальні паролі, які відповідають вимогам безпеки, для кожного вебсайту та сервісу. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Переклад багатьма мовами -Завдяки нашій глобальній спільноті, Bitwarden перекладено 40 мовами, і їх кількість зростає. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Програми для різних платформ -Зберігайте і діліться важливими даними, а також користуйтеся іншими можливостями у вашому сховищі Bitwarden в будь-якому браузері, мобільному пристрої, чи комп'ютерній операційній системі. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Захищений, безплатний менеджер паролів для всіх ваших пристроїв + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Синхронізуйте й отримуйте доступ до свого сховища на різних пристроях diff --git a/apps/browser/store/locales/vi/copy.resx b/apps/browser/store/locales/vi/copy.resx index 220d50bdfa..e0403d1f32 100644 --- a/apps/browser/store/locales/vi/copy.resx +++ b/apps/browser/store/locales/vi/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Trình quản lý mật khẩu miễn phí + Bitwarden Password Manager - Một trình quản lý mật khẩu an toàn và miễn phí cho mọi thiết bị của bạn + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc là công ty mẹ của 8bit Solutions LLC + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -ĐƯỢC ĐÁNH GIÁ LÀ TRÌNH QUẢN LÝ MẬT KHẨU TỐT NHẤT BỞI NHÀ BÁO LỚN NHƯ THE VERGE, CNET, U.S. NEWS & WORLD REPORT VÀ HƠN NỮA +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Quản lý, lưu trữ, bảo mật và chia sẻ mật khẩu không giới hạn trên các thiết bị không giới hạn mọi lúc, mọi nơi. Bitwarden cung cấp các giải pháp quản lý mật khẩu mã nguồn mở cho tất cả mọi người, cho dù ở nhà, tại cơ quan hay khi đang di chuyển. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Tạo mật khẩu mạnh, không bị trùng và ngẫu nhiên dựa trên các yêu cầu bảo mật cho mọi trang web bạn thường xuyên sử dụng. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Tính năng 'Bitwarden Send' nhanh chóng truyền thông tin được mã hóa --- tệp và văn bản - trực tiếp đến bất kỳ ai. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden cung cấp các gói 'Nhóm' và 'Doanh nghiệp' cho các công ty để bạn có thể chia sẻ mật khẩu với đồng nghiệp một cách an toàn. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Tại sao bạn nên chọn Bitwarden: -Mã hóa tốt nhất thế giới -Mật khẩu được bảo vệ bằng mã hóa đầu cuối (end-to-end encryption) tiên tiến như AES-256 bit, salted hashtag, và PBKDF2 SHA-256 nên dữ liệu của bạn luôn an toàn và riêng tư. +More reasons to choose Bitwarden: -Trình tạo mật khẩu tích hợp -Tạo mật khẩu mạnh, không bị trùng lặp, và ngẫu nhiên dựa trên các yêu cầu bảo mật cho mọi trang web mà bạn thường xuyên sử dụng. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Bản dịch ngôn ngữ từ cộng đồng -Bitwarden đã có bản dịch 40 ngôn ngữ và đang phát triển nhờ vào cộng đồng toàn cầu của chúng tôi. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Ứng dụng đa nền tảng -Bảo mật và chia sẻ dữ liệu nhạy cảm trong kho lưu trữ Bitwarden của bạn từ bất kỳ trình duyệt, điện thoại thông minh hoặc hệ điều hành máy tính nào, và hơn thế nữa. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Một trình quản lý mật khẩu an toàn và miễn phí cho mọi thiết bị của bạn + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Đồng bộ hóa và truy cập vào kho lưu trữ của bạn từ nhiều thiết bị diff --git a/apps/browser/store/locales/zh_CN/copy.resx b/apps/browser/store/locales/zh_CN/copy.resx index e424ef743a..94543f8f6f 100644 --- a/apps/browser/store/locales/zh_CN/copy.resx +++ b/apps/browser/store/locales/zh_CN/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – 免费密码管理器 + Bitwarden Password Manager - 安全免费的跨平台密码管理器 + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. 是 8bit Solutions LLC 的母公司。 + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -被 THE VERGE、U.S. NEWS & WORLD REPORT、CNET 等评为最佳的密码管理器。 +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -从任何地方,不限制设备,管理、存储、保护和共享无限的密码。Bitwarden 为每个人提供开源的密码管理解决方案,无论是在家里,在工作中,还是在旅途中。 +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -基于安全要求,为您经常访问的每个网站生成强大、唯一和随机的密码。 +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send 快速传输加密的信息---文件和文本---直接给任何人。 +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden 为公司提供团队和企业计划,因此您可以安全地与同事共享密码。 +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -为何选择 Bitwarden: -世界级的加密技术 -密码受到先进的端到端加密(AES-256 位、盐化标签和 PBKDF2 SHA-256)的保护,为您的数据保持安全和隐密。 +More reasons to choose Bitwarden: -内置密码生成器 -基于安全要求,为您经常访问的每个网站生成强大、唯一和随机的密码。 +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -全球翻译 -Bitwarden 的翻译有 40 种语言,而且还在不断增加,感谢我们的全球社区。 +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -跨平台应用程序 -从任何浏览器、移动设备或桌面操作系统,以及更多的地方,在您的 Bitwarden 密码库中保护和分享敏感数据。 +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - 安全免费的跨平台密码管理器 + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. 从多台设备同步和访问密码库 diff --git a/apps/browser/store/locales/zh_TW/copy.resx b/apps/browser/store/locales/zh_TW/copy.resx index be39fdca06..ab37ed5f7b 100644 --- a/apps/browser/store/locales/zh_TW/copy.resx +++ b/apps/browser/store/locales/zh_TW/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – 免費密碼管理工具 + Bitwarden Password Manager - 安全、免費、跨平台的密碼管理工具 + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. 是 8bit Solutions LLC 的母公司。 + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -被 THE VERGE、U.S. NEWS & WORLD REPORT、CNET 等評為最佳的密碼管理器。 +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -從任何地方,不限制設備,管理、存儲、保護和共享無限的密碼。Bitwarden 為每個人提供開源的密碼管理解決方案,無論是在家裡,在工作中,還是在旅途中。 +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -基於安全要求,為您經常訪問的每個網站生成強大、唯一和隨機的密碼。 +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send 快速傳輸加密的信息---文檔和文本---直接給任何人。 +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden 為公司提供團隊和企業計劃,因此您可以安全地與同事共享密碼。 +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -為何選擇 Bitwarden: -世界級的加密技術 -密碼受到先進的端到端加密(AES-256 位、鹽化標籤和 PBKDF2 SHA-256)的保護,為您的資料保持安全和隱密。 +More reasons to choose Bitwarden: -內置密碼生成器 -基於安全要求,為您經常訪問的每個網站生成強大、唯一和隨機的密碼。 +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -全球翻譯 -Bitwarden 的翻譯有 40 種語言,而且還在不斷增加,感謝我們的全球社區。 +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -跨平台應用程式 -從任何瀏覽器、行動裝置或桌面作業系統,以及更多的地方,在您的 Bitwarden 密碼庫中保護和分享敏感資料。 +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - 安全、免費、跨平台的密碼管理工具 + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. 在多部裝置上同步和存取密碼庫 diff --git a/apps/browser/store/windows/AppxManifest.xml b/apps/browser/store/windows/AppxManifest.xml index f57b3db988..df02ea085c 100644 --- a/apps/browser/store/windows/AppxManifest.xml +++ b/apps/browser/store/windows/AppxManifest.xml @@ -11,7 +11,7 @@ Version="0.0.0.0"/> - Bitwarden Extension - Free Password Manager + Bitwarden Password Manager 8bit Solutions LLC Assets/icon_50.png @@ -30,10 +30,10 @@ @@ -41,7 +41,7 @@ + DisplayName="Bitwarden Password Manager"> diff --git a/apps/browser/test.setup.ts b/apps/browser/test.setup.ts index 1031268186..4800b4c17f 100644 --- a/apps/browser/test.setup.ts +++ b/apps/browser/test.setup.ts @@ -68,6 +68,8 @@ const tabs = { const scripting = { executeScript: jest.fn(), + registerContentScripts: jest.fn(), + unregisterContentScripts: jest.fn(), }; const windows = { @@ -124,6 +126,19 @@ const offscreen = { }, }; +const permissions = { + contains: jest.fn((permissions, callback) => { + callback(true); + }), +}; + +const webNavigation = { + onCommitted: { + addListener: jest.fn(), + removeListener: jest.fn(), + }, +}; + // set chrome global.chrome = { i18n, @@ -137,4 +152,6 @@ global.chrome = { privacy, extension, offscreen, + permissions, + webNavigation, } as any; diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json index 694246f59a..505f1533ae 100644 --- a/apps/browser/tsconfig.json +++ b/apps/browser/tsconfig.json @@ -9,6 +9,7 @@ "allowJs": true, "sourceMap": true, "baseUrl": ".", + "lib": ["ES2021.String"], "paths": { "@bitwarden/admin-console": ["../../libs/admin-console/src"], "@bitwarden/angular/*": ["../../libs/angular/src/*"], diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js index 3b5724b198..2756ab4395 100644 --- a/apps/browser/webpack.config.js +++ b/apps/browser/webpack.config.js @@ -166,8 +166,6 @@ const mainConfig = { "content/notificationBar": "./src/autofill/content/notification-bar.ts", "content/contextMenuHandler": "./src/autofill/content/context-menu-handler.ts", "content/content-message-handler": "./src/autofill/content/content-message-handler.ts", - "content/fido2/trigger-fido2-content-script-injection": - "./src/vault/fido2/content/trigger-fido2-content-script-injection.ts", "content/fido2/content-script": "./src/vault/fido2/content/content-script.ts", "content/fido2/page-script": "./src/vault/fido2/content/page-script.ts", "notification/bar": "./src/autofill/notification/bar.ts", @@ -277,6 +275,8 @@ if (manifestVersion == 2) { mainConfig.entry.background = "./src/platform/background.ts"; mainConfig.entry["content/lp-suppress-import-download-script-append-mv2"] = "./src/tools/content/lp-suppress-import-download-script-append.mv2.ts"; + mainConfig.entry["content/fido2/page-script-append-mv2"] = + "./src/vault/fido2/content/page-script-append.mv2.ts"; configs.push(mainConfig); } else { diff --git a/apps/cli/src/auth/commands/unlock.command.ts b/apps/cli/src/auth/commands/unlock.command.ts index 98bc926079..d52468139a 100644 --- a/apps/cli/src/auth/commands/unlock.command.ts +++ b/apps/cli/src/auth/commands/unlock.command.ts @@ -1,6 +1,10 @@ +import { firstValueFrom } from "rxjs"; + import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -18,6 +22,8 @@ import { CliUtils } from "../../utils"; export class UnlockCommand { constructor( + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private stateService: StateService, private cryptoFunctionService: CryptoFunctionService, @@ -45,11 +51,14 @@ export class UnlockCommand { const kdf = await this.stateService.getKdfType(); const kdfConfig = await this.stateService.getKdfConfig(); const masterKey = await this.cryptoService.makeMasterKey(password, email, kdf, kdfConfig); - const storedKeyHash = await this.cryptoService.getMasterKeyHash(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + const storedMasterKeyHash = await firstValueFrom( + this.masterPasswordService.masterKeyHash$(userId), + ); let passwordValid = false; if (masterKey != null) { - if (storedKeyHash != null) { + if (storedMasterKeyHash != null) { passwordValid = await this.cryptoService.compareAndUpdateKeyHash(password, masterKey); } else { const serverKeyHash = await this.cryptoService.hashMasterKey( @@ -67,7 +76,7 @@ export class UnlockCommand { masterKey, HashPurpose.LocalAuthorization, ); - await this.cryptoService.setMasterKeyHash(localKeyHash); + await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId); } catch { // Ignore } @@ -75,7 +84,7 @@ export class UnlockCommand { } if (passwordValid) { - await this.cryptoService.setMasterKey(masterKey); + await this.masterPasswordService.setMasterKey(masterKey, userId); const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); await this.cryptoService.setUserKey(userKey); diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 3815fc773b..c3c4042adf 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -18,22 +18,26 @@ import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation"; import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; +import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service"; import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; +import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service"; @@ -56,10 +60,10 @@ import { } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { KeySuffixOptions, LogLevelType } from "@bitwarden/common/platform/enums"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { MessageSender } from "@bitwarden/common/platform/messaging"; import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; -import { BroadcasterService } from "@bitwarden/common/platform/services/broadcaster.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; @@ -71,7 +75,6 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; -import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service"; import { StateService } from "@bitwarden/common/platform/services/state.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { @@ -151,7 +154,7 @@ global.DOMParser = new jsdom.JSDOM().window.DOMParser; const packageJson = require("../package.json"); export class Main { - messagingService: NoopMessagingService; + messagingService: MessageSender; storageService: LowdbStorageService; secureStorageService: NodeEnvSecureStorageService; memoryStorageService: MemoryStorageService; @@ -168,6 +171,7 @@ export class Main { organizationUserService: OrganizationUserService; collectionService: CollectionService; vaultTimeoutService: VaultTimeoutService; + masterPasswordService: InternalMasterPasswordServiceAbstraction; vaultTimeoutSettingsService: VaultTimeoutSettingsService; syncService: SyncService; eventCollectionService: EventCollectionServiceAbstraction; @@ -207,7 +211,6 @@ export class Main { organizationService: OrganizationService; providerService: ProviderService; twoFactorService: TwoFactorService; - broadcasterService: BroadcasterService; folderApiService: FolderApiService; userVerificationApiService: UserVerificationApiService; organizationApiService: OrganizationApiServiceAbstraction; @@ -229,6 +232,7 @@ export class Main { stateEventRunnerService: StateEventRunnerService; biometricStateService: BiometricStateService; billingAccountProfileStateService: BillingAccountProfileStateService; + providerApiService: ProviderApiServiceAbstraction; constructor() { let p = null; @@ -292,7 +296,7 @@ export class Main { stateEventRegistrarService, ); - this.messagingService = new NoopMessagingService(); + this.messagingService = MessageSender.EMPTY; this.accountService = new AccountServiceImplementation( this.messagingService, @@ -351,7 +355,10 @@ export class Main { migrationRunner, ); + this.masterPasswordService = new MasterPasswordService(this.stateProvider); + this.cryptoService = new CryptoService( + this.masterPasswordService, this.keyGenerationService, this.cryptoFunctionService, this.encryptService, @@ -411,9 +418,7 @@ export class Main { this.sendService, ); - this.searchService = new SearchService(this.logService, this.i18nService); - - this.broadcasterService = new BroadcasterService(); + this.searchService = new SearchService(this.logService, this.i18nService, this.stateProvider); this.collectionService = new CollectionService( this.cryptoService, @@ -432,6 +437,8 @@ export class Main { this.policyApiService = new PolicyApiService(this.policyService, this.apiService); this.keyConnectorService = new KeyConnectorService( + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -471,9 +478,11 @@ export class Main { this.authRequestService = new AuthRequestService( this.appIdService, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, - this.stateService, + this.stateProvider, ); this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( @@ -481,6 +490,8 @@ export class Main { ); this.loginStrategyService = new LoginStrategyService( + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -532,13 +543,13 @@ export class Main { this.encryptService, this.cipherFileUploadService, this.configService, + this.stateProvider, ); this.folderService = new FolderService( this.cryptoService, this.i18nService, this.cipherService, - this.stateService, this.stateProvider, ); @@ -568,6 +579,8 @@ export class Main { this.userVerificationService = new UserVerificationService( this.stateService, this.cryptoService, + this.accountService, + this.masterPasswordService, this.i18nService, this.userVerificationApiService, this.userDecryptionOptionsService, @@ -578,6 +591,8 @@ export class Main { ); this.vaultTimeoutService = new VaultTimeoutService( + this.accountService, + this.masterPasswordService, this.cipherService, this.folderService, this.collectionService, @@ -596,6 +611,8 @@ export class Main { this.avatarService = new AvatarService(this.apiService, this.stateProvider); this.syncService = new SyncService( + this.masterPasswordService, + this.accountService, this.apiService, this.domainSettingsService, this.folderService, @@ -616,6 +633,7 @@ export class Main { this.avatarService, async (expired: boolean) => await this.logout(), this.billingAccountProfileStateService, + this.tokenService, ); this.totpService = new TotpService(this.cryptoFunctionService, this.logService); @@ -664,7 +682,7 @@ export class Main { this.apiService, this.stateProvider, this.logService, - this.accountService, + this.authService, ); this.eventCollectionService = new EventCollectionService( @@ -672,8 +690,10 @@ export class Main { this.stateProvider, this.organizationService, this.eventUploadService, - this.accountService, + this.authService, ); + + this.providerApiService = new ProviderApiService(this.apiService); } async run() { @@ -702,9 +722,7 @@ export class Main { this.cipherService.clear(userId), this.folderService.clear(userId), this.collectionService.clear(userId as UserId), - this.policyService.clear(userId as UserId), this.passwordGenerationService.clear(), - this.providerService.save(null, userId as UserId), ]); await this.stateEventRunnerService.handleEvent("logout", userId as UserId); diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index 3d4f9529ad..e64ff8b551 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -86,8 +86,7 @@ export class EditCommand { cipherView = CipherExport.toView(req, cipherView); const encCipher = await this.cipherService.encrypt(cipherView); try { - await this.cipherService.updateWithServer(encCipher); - const updatedCipher = await this.cipherService.get(cipher.id); + const updatedCipher = await this.cipherService.updateWithServer(encCipher); const decCipher = await updatedCipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher), ); @@ -111,8 +110,7 @@ export class EditCommand { cipher.collectionIds = req; try { - await this.cipherService.saveCollectionsWithServer(cipher); - const updatedCipher = await this.cipherService.get(cipher.id); + const updatedCipher = await this.cipherService.saveCollectionsWithServer(cipher); const decCipher = await updatedCipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher), ); diff --git a/apps/cli/src/commands/serve.command.ts b/apps/cli/src/commands/serve.command.ts index 4d0d1e5798..76447f769c 100644 --- a/apps/cli/src/commands/serve.command.ts +++ b/apps/cli/src/commands/serve.command.ts @@ -122,6 +122,8 @@ export class ServeCommand { this.shareCommand = new ShareCommand(this.main.cipherService); this.lockCommand = new LockCommand(this.main.vaultTimeoutService); this.unlockCommand = new UnlockCommand( + this.main.accountService, + this.main.masterPasswordService, this.main.cryptoService, this.main.stateService, this.main.cryptoFunctionService, diff --git a/apps/cli/src/locales/en/messages.json b/apps/cli/src/locales/en/messages.json index e12b30af2c..8364e0b328 100644 --- a/apps/cli/src/locales/en/messages.json +++ b/apps/cli/src/locales/en/messages.json @@ -49,5 +49,14 @@ }, "unsupportedEncryptedImport": { "message": "Importing encrypted files is currently not supported." + }, + "importUnassignedItemsError": { + "message": "File contains unassigned items." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/cli/src/platform/flags.ts b/apps/cli/src/platform/flags.ts index 4e31e39e99..dc0103e243 100644 --- a/apps/cli/src/platform/flags.ts +++ b/apps/cli/src/platform/flags.ts @@ -7,11 +7,11 @@ import { } from "@bitwarden/common/platform/misc/flags"; // required to avoid linting errors when there are no flags -/* eslint-disable-next-line @typescript-eslint/ban-types */ +// eslint-disable-next-line @typescript-eslint/ban-types export type Flags = {} & SharedFlags; // required to avoid linting errors when there are no flags -/* eslint-disable-next-line @typescript-eslint/ban-types */ +// eslint-disable-next-line @typescript-eslint/ban-types export type DevFlags = {} & SharedDevFlags; export function flagEnabled(flag: keyof Flags): boolean { diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts index a79f3847da..fa71a88f54 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -253,6 +253,8 @@ export class Program { if (!cmd.check) { await this.exitIfNotAuthed(); const command = new UnlockCommand( + this.main.accountService, + this.main.masterPasswordService, this.main.cryptoService, this.main.stateService, this.main.cryptoFunctionService, @@ -613,6 +615,8 @@ export class Program { this.processResponse(response, true); } else { const command = new UnlockCommand( + this.main.accountService, + this.main.masterPasswordService, this.main.cryptoService, this.main.stateService, this.main.cryptoFunctionService, diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts index b813227109..78ee04e73c 100644 --- a/apps/cli/src/vault/create.command.ts +++ b/apps/cli/src/vault/create.command.ts @@ -80,8 +80,7 @@ export class CreateCommand { private async createCipher(req: CipherExport) { const cipher = await this.cipherService.encrypt(CipherExport.toView(req)); try { - await this.cipherService.createWithServer(cipher); - const newCipher = await this.cipherService.get(cipher.id); + const newCipher = await this.cipherService.createWithServer(cipher); const decCipher = await newCipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(newCipher), ); @@ -142,12 +141,11 @@ export class CreateCommand { } try { - await this.cipherService.saveAttachmentRawWithServer( + const updatedCipher = await this.cipherService.saveAttachmentRawWithServer( cipher, fileName, new Uint8Array(fileBuf).buffer, ); - const updatedCipher = await this.cipherService.get(cipher.id); const decCipher = await updatedCipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher), ); diff --git a/apps/desktop/config/development.json b/apps/desktop/config/development.json index d2b1073812..7a8659feff 100644 --- a/apps/desktop/config/development.json +++ b/apps/desktop/config/development.json @@ -1,7 +1,6 @@ { "devFlags": {}, "flags": { - "showDDGSetting": true, "enableCipherKeyEncryption": false } } diff --git a/apps/desktop/config/production.json b/apps/desktop/config/production.json index 39b78094d0..f57c3d9bc3 100644 --- a/apps/desktop/config/production.json +++ b/apps/desktop/config/production.json @@ -1,6 +1,5 @@ { "flags": { - "showDDGSetting": true, "enableCipherKeyEncryption": false } } diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 7646b63001..b921cab37b 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -1237,18 +1237,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.51" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.51" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index a1625020e5..4b2bc2e905 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -24,7 +24,7 @@ rand = "=0.8.5" retry = "=2.0.0" scopeguard = "=1.2.0" sha2 = "=0.10.8" -thiserror = "=1.0.51" +thiserror = "=1.0.58" typenum = "=1.17.0" [build-dependencies] diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 5ce5ef948f..4f0d05581c 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -24,7 +24,7 @@ "**/node_modules/argon2/package.json", "**/node_modules/argon2/lib/binding/napi-v3/argon2.node" ], - "electronVersion": "28.2.8", + "electronVersion": "28.3.1", "generateUpdatesFilesForAllChannels": true, "publish": { "provider": "generic", @@ -203,7 +203,7 @@ "si", "sk", "sl", - "sr", + "sr-cyrl", "sv", "te", "th", @@ -228,6 +228,7 @@ "artifactName": "${productName}-${version}-${arch}.${ext}" }, "snap": { + "summary": "After installation enable required `password-manager-service` by running `sudo snap connect bitwarden:password-manager-service`.", "autoStart": true, "base": "core22", "confinement": "strict", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 52dd0fafdb..4bb0ab2d93 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.3.2", + "version": "2024.4.2", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index a613328878..f3958d7c87 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -3,6 +3,7 @@ import { FormBuilder } from "@angular/forms"; import { BehaviorSubject, firstValueFrom, Observable, Subject } from "rxjs"; import { concatMap, debounceTime, filter, map, switchMap, takeUntil, tap } from "rxjs/operators"; +import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -13,6 +14,7 @@ import { DeviceType } from "@bitwarden/common/enums"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @@ -20,12 +22,13 @@ import { BiometricStateService } from "@bitwarden/common/platform/biometrics/bio import { ThemeType, KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { DialogService } from "@bitwarden/components"; import { SetPinComponent } from "../../auth/components/set-pin.component"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; -import { flagEnabled } from "../../platform/flags"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; +import { NativeMessagingManifestService } from "../services/native-messaging-manifest.service"; @Component({ selector: "app-settings", @@ -61,6 +64,7 @@ export class SettingsComponent implements OnInit { showAppPreferences = true; currentUserEmail: string; + currentUserId: UserId; availableVaultTimeoutActions$: Observable; vaultTimeoutPolicyCallout: Observable<{ @@ -123,6 +127,9 @@ export class SettingsComponent implements OnInit { private desktopSettingsService: DesktopSettingsService, private biometricStateService: BiometricStateService, private desktopAutofillSettingsService: DesktopAutofillSettingsService, + private authRequestService: AuthRequestServiceAbstraction, + private logService: LogService, + private nativeMessagingManifestService: NativeMessagingManifestService, ) { const isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop; @@ -146,7 +153,7 @@ export class SettingsComponent implements OnInit { this.startToTrayDescText = this.i18nService.t(startToTrayKey + "Desc"); // DuckDuckGo browser is only for macos initially - this.showDuckDuckGoIntegrationOption = flagEnabled("showDDGSetting") && isMac; + this.showDuckDuckGoIntegrationOption = isMac; this.vaultTimeoutOptions = [ // { name: i18nService.t('immediately'), value: 0 }, @@ -208,6 +215,7 @@ export class SettingsComponent implements OnInit { return; } this.currentUserEmail = await this.stateService.getEmail(); + this.currentUserId = (await this.stateService.getUserId()) as UserId; this.availableVaultTimeoutActions$ = this.refreshTimeoutSettings$.pipe( switchMap(() => this.vaultTimeoutSettingsService.availableVaultTimeoutActions$()), @@ -250,7 +258,8 @@ export class SettingsComponent implements OnInit { requirePasswordOnStart: await firstValueFrom( this.biometricStateService.requirePasswordOnStart$, ), - approveLoginRequests: (await this.stateService.getApproveLoginRequests()) ?? false, + approveLoginRequests: + (await this.authRequestService.getAcceptAuthRequests(this.currentUserId)) ?? false, clearClipboard: await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$), minimizeOnCopyToClipboard: await this.stateService.getMinimizeOnCopyToClipboard(), enableFavicons: await firstValueFrom(this.domainSettingsService.showFavicons$), @@ -623,11 +632,20 @@ export class SettingsComponent implements OnInit { } await this.stateService.setEnableBrowserIntegration(this.form.value.enableBrowserIntegration); - this.messagingService.send( - this.form.value.enableBrowserIntegration - ? "enableBrowserIntegration" - : "disableBrowserIntegration", + + const errorResult = await this.nativeMessagingManifestService.generate( + this.form.value.enableBrowserIntegration, ); + if (errorResult !== null) { + this.logService.error("Error in browser integration: " + errorResult); + await this.dialogService.openSimpleDialog({ + title: { key: "browserIntegrationErrorTitle" }, + content: { key: "browserIntegrationErrorDesc" }, + acceptButtonText: { key: "ok" }, + cancelButtonText: null, + type: "danger", + }); + } if (!this.form.value.enableBrowserIntegration) { this.form.controls.enableBrowserIntegrationFingerprint.setValue(false); @@ -642,15 +660,28 @@ export class SettingsComponent implements OnInit { this.form.value.enableDuckDuckGoBrowserIntegration, ); + // Adding to cover users on a previous version of DDG + await this.stateService.setEnableDuckDuckGoBrowserIntegration( + this.form.value.enableDuckDuckGoBrowserIntegration, + ); + if (!this.form.value.enableBrowserIntegration) { await this.stateService.setDuckDuckGoSharedKey(null); } - this.messagingService.send( - this.form.value.enableDuckDuckGoBrowserIntegration - ? "enableDuckDuckGoBrowserIntegration" - : "disableDuckDuckGoBrowserIntegration", + const errorResult = await this.nativeMessagingManifestService.generateDuckDuckGo( + this.form.value.enableDuckDuckGoBrowserIntegration, ); + if (errorResult !== null) { + this.logService.error("Error in DDG browser integration: " + errorResult); + await this.dialogService.openSimpleDialog({ + title: { key: "browserIntegrationUnsupportedTitle" }, + content: errorResult.message, + acceptButtonText: { key: "ok" }, + cancelButtonText: null, + type: "warning", + }); + } } async saveBrowserIntegrationFingerprint() { @@ -666,7 +697,10 @@ export class SettingsComponent implements OnInit { } async updateApproveLoginRequests() { - await this.stateService.setApproveLoginRequests(this.form.value.approveLoginRequests); + await this.authRequestService.setAcceptAuthRequests( + this.form.value.approveLoginRequests, + this.currentUserId, + ); } ngOnDestroy() { diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 4e74135c49..ad99a3a447 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -3,14 +3,11 @@ import { NgZone, OnDestroy, OnInit, - SecurityContext, Type, ViewChild, ViewContainerRef, } from "@angular/core"; -import { DomSanitizer } from "@angular/platform-browser"; import { Router } from "@angular/router"; -import { IndividualConfig, ToastrService } from "ngx-toastr"; import { firstValueFrom, Subject, takeUntil } from "rxjs"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; @@ -26,6 +23,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -48,7 +46,7 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { DeleteAccountComponent } from "../auth/delete-account.component"; import { LoginApprovalComponent } from "../auth/login/login-approval.component"; @@ -120,6 +118,7 @@ export class AppComponent implements OnInit, OnDestroy { private accountCleanUpInProgress: { [userId: string]: boolean } = {}; constructor( + private masterPasswordService: MasterPasswordServiceAbstraction, private broadcasterService: BroadcasterService, private folderService: InternalFolderService, private syncService: SyncService, @@ -127,9 +126,8 @@ export class AppComponent implements OnInit, OnDestroy { private cipherService: CipherService, private authService: AuthService, private router: Router, - private toastrService: ToastrService, + private toastService: ToastService, private i18nService: I18nService, - private sanitizer: DomSanitizer, private ngZone: NgZone, private vaultTimeoutService: VaultTimeoutService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, @@ -292,7 +290,7 @@ export class AppComponent implements OnInit, OnDestroy { ); break; case "showToast": - this.showToast(message); + this.toastService._showToast(message); break; case "copiedToClipboard": if (!message.clearing) { @@ -408,8 +406,9 @@ export class AppComponent implements OnInit, OnDestroy { (await this.authService.getAuthStatus(message.userId)) === AuthenticationStatus.Locked; const forcedPasswordReset = - (await this.stateService.getForceSetPasswordReason({ userId: message.userId })) != - ForceSetPasswordReason.None; + (await firstValueFrom( + this.masterPasswordService.forceSetPasswordReason$(message.userId), + )) != ForceSetPasswordReason.None; if (locked) { this.messagingService.send("locked", { userId: message.userId }); } else if (forcedPasswordReset) { @@ -583,9 +582,7 @@ export class AppComponent implements OnInit, OnDestroy { await this.collectionService.clear(userBeingLoggedOut); await this.passwordGenerationService.clear(userBeingLoggedOut); await this.vaultTimeoutSettingsService.clear(userBeingLoggedOut); - await this.policyService.clear(userBeingLoggedOut); await this.biometricStateService.logout(userBeingLoggedOut as UserId); - await this.providerService.save(null, userBeingLoggedOut as UserId); await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut as UserId); @@ -608,7 +605,6 @@ export class AppComponent implements OnInit, OnDestroy { // This must come last otherwise the logout will prematurely trigger // a process reload before all the state service user data can be cleaned up if (userBeingLoggedOut === preLogoutActiveUserId) { - this.searchService.clearIndex(); this.authService.logOut(async () => { if (expired) { this.platformUtilsService.showToast( @@ -674,34 +670,6 @@ export class AppComponent implements OnInit, OnDestroy { }); } - private showToast(msg: any) { - let message = ""; - - const options: Partial = {}; - - if (typeof msg.text === "string") { - message = msg.text; - } else if (msg.text.length === 1) { - message = msg.text[0]; - } else { - msg.text.forEach( - (t: string) => - (message += "

" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "

"), - ); - options.enableHtml = true; - } - if (msg.options != null) { - if (msg.options.trustedHtml === true) { - options.enableHtml = true; - } - if (msg.options.timeout != null && msg.options.timeout > 0) { - options.timeOut = msg.options.timeout; - } - } - - this.toastrService.show(message, msg.title, options, "toast-" + msg.type); - } - private routeToVault(action: string, cipherType: CipherType) { if (!this.router.url.includes("vault")) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index d1a83d468c..ae2e1ba97c 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -12,6 +12,7 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -35,6 +36,7 @@ export class InitService { private nativeMessagingService: NativeMessagingService, private themingService: AbstractThemingService, private encryptService: EncryptService, + private userKeyInitService: UserKeyInitService, @Inject(DOCUMENT) private document: Document, ) {} @@ -42,6 +44,8 @@ export class InitService { return async () => { this.nativeMessagingService.init(); await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process + this.userKeyInitService.listenForActiveUserChangesToSetUserKey(); + // 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.syncService.fullSync(true); diff --git a/apps/desktop/src/app/services/native-messaging-manifest.service.ts b/apps/desktop/src/app/services/native-messaging-manifest.service.ts new file mode 100644 index 0000000000..6cc58a581b --- /dev/null +++ b/apps/desktop/src/app/services/native-messaging-manifest.service.ts @@ -0,0 +1,13 @@ +import { Injectable } from "@angular/core"; + +@Injectable() +export class NativeMessagingManifestService { + constructor() {} + + async generate(create: boolean): Promise { + return ipc.platform.nativeMessaging.manifests.generate(create); + } + async generateDuckDuckGo(create: boolean): Promise { + return ipc.platform.nativeMessaging.manifests.generateDuckDuckGo(create); + } +} diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 84932ce7d9..d1d51c0f1c 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -1,4 +1,5 @@ import { APP_INITIALIZER, NgModule } from "@angular/core"; +import { Subject, merge } from "rxjs"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { @@ -14,15 +15,16 @@ import { SYSTEM_THEME_OBSERVABLE, SafeInjectionToken, STATE_FACTORY, + INTRAPROCESS_MESSAGING_SUBJECT, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -41,6 +43,9 @@ import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/ import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- Used for dependency injection +import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; @@ -62,11 +67,12 @@ import { ELECTRON_SUPPORTS_SECURE_STORAGE, ElectronPlatformUtilsService, } from "../../platform/services/electron-platform-utils.service"; -import { ElectronRendererMessagingService } from "../../platform/services/electron-renderer-messaging.service"; +import { ElectronRendererMessageSender } from "../../platform/services/electron-renderer-message.sender"; import { ElectronRendererSecureStorageService } from "../../platform/services/electron-renderer-secure-storage.service"; import { ElectronRendererStorageService } from "../../platform/services/electron-renderer-storage.service"; import { ElectronStateService } from "../../platform/services/electron-state.service"; import { I18nRendererService } from "../../platform/services/i18n.renderer.service"; +import { fromIpcMessaging } from "../../platform/utils/from-ipc-messaging"; import { fromIpcSystemTheme } from "../../platform/utils/from-ipc-system-theme"; import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service"; import { NativeMessageHandlerService } from "../../services/native-message-handler.service"; @@ -75,6 +81,7 @@ import { SearchBarService } from "../layout/search/search-bar.service"; import { DesktopFileDownloadService } from "./desktop-file-download.service"; import { InitService } from "./init.service"; +import { NativeMessagingManifestService } from "./native-messaging-manifest.service"; import { RendererCryptoFunctionService } from "./renderer-crypto-function.service"; const RELOAD_CALLBACK = new SafeInjectionToken<() => any>("RELOAD_CALLBACK"); @@ -136,9 +143,24 @@ const safeProviders: SafeProvider[] = [ deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY, GlobalStateProvider], }), safeProvider({ - provide: MessagingServiceAbstraction, - useClass: ElectronRendererMessagingService, - deps: [BroadcasterServiceAbstraction], + provide: MessageSender, + useFactory: (subject: Subject>) => + MessageSender.combine( + new ElectronRendererMessageSender(), // Communication with main process + new SubjectMessageSender(subject), // Communication with ourself + ), + deps: [INTRAPROCESS_MESSAGING_SUBJECT], + }), + safeProvider({ + provide: MessageListener, + useFactory: (subject: Subject>) => + new MessageListener( + merge( + subject.asObservable(), // For messages from the same context + fromIpcMessaging(), // For messages from the main process + ), + ), + deps: [INTRAPROCESS_MESSAGING_SUBJECT], }), safeProvider({ provide: AbstractStorageService, @@ -228,6 +250,7 @@ const safeProviders: SafeProvider[] = [ provide: CryptoServiceAbstraction, useClass: ElectronCryptoService, deps: [ + InternalMasterPasswordServiceAbstraction, KeyGenerationServiceAbstraction, CryptoFunctionServiceAbstraction, EncryptService, @@ -247,6 +270,11 @@ const safeProviders: SafeProvider[] = [ provide: DesktopAutofillSettingsService, deps: [StateProvider], }), + safeProvider({ + provide: NativeMessagingManifestService, + useClass: NativeMessagingManifestService, + deps: [], + }), ]; @NgModule({ diff --git a/apps/desktop/src/app/tools/generator.component.spec.ts b/apps/desktop/src/app/tools/generator.component.spec.ts index 53f919a596..51b5bf93a2 100644 --- a/apps/desktop/src/app/tools/generator.component.spec.ts +++ b/apps/desktop/src/app/tools/generator.component.spec.ts @@ -10,6 +10,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { GeneratorComponent } from "./generator.component"; @@ -54,6 +55,10 @@ describe("GeneratorComponent", () => { provide: LogService, useValue: mock(), }, + { + provide: CipherService, + useValue: mock(), + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/apps/desktop/src/auth/delete-account.component.html b/apps/desktop/src/auth/delete-account.component.html index 8639b0f5be..42c06b7489 100644 --- a/apps/desktop/src/auth/delete-account.component.html +++ b/apps/desktop/src/auth/delete-account.component.html @@ -7,7 +7,7 @@ {{ "deleteAccountWarning" | i18n }} -
+
{ let broadcasterServiceMock: MockProxy; let platformUtilsServiceMock: MockProxy; let activatedRouteMock: MockProxy; + let mockMasterPasswordService: FakeMasterPasswordService; const mockUserId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); @@ -67,6 +70,8 @@ describe("LockComponent", () => { activatedRouteMock = mock(); activatedRouteMock.queryParams = mock(); + mockMasterPasswordService = new FakeMasterPasswordService(); + biometricStateService.dismissedRequirePasswordOnStartCallout$ = of(false); biometricStateService.promptAutomatically$ = of(false); biometricStateService.promptCancelled$ = of(false); @@ -74,6 +79,7 @@ describe("LockComponent", () => { await TestBed.configureTestingModule({ declarations: [LockComponent, I18nPipe], providers: [ + { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, { provide: I18nService, useValue: mock(), diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts index 8b1448c06f..16b58c5bbe 100644 --- a/apps/desktop/src/auth/lock.component.ts +++ b/apps/desktop/src/auth/lock.component.ts @@ -11,6 +11,7 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { DeviceType } from "@bitwarden/common/enums"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; @@ -38,6 +39,7 @@ export class LockComponent extends BaseLockComponent { private autoPromptBiometric = false; constructor( + masterPasswordService: InternalMasterPasswordServiceAbstraction, router: Router, i18nService: I18nService, platformUtilsService: PlatformUtilsService, @@ -63,6 +65,7 @@ export class LockComponent extends BaseLockComponent { accountService: AccountService, ) { super( + masterPasswordService, router, i18nService, platformUtilsService, diff --git a/apps/desktop/src/auth/set-password.component.ts b/apps/desktop/src/auth/set-password.component.ts index a75668a856..93dfe0abd8 100644 --- a/apps/desktop/src/auth/set-password.component.ts +++ b/apps/desktop/src/auth/set-password.component.ts @@ -8,6 +8,8 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -29,6 +31,8 @@ const BroadcasterSubscriptionId = "SetPasswordComponent"; }) export class SetPasswordComponent extends BaseSetPasswordComponent implements OnDestroy { constructor( + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, apiService: ApiService, i18nService: I18nService, cryptoService: CryptoService, @@ -50,6 +54,8 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On dialogService: DialogService, ) { super( + accountService, + masterPasswordService, i18nService, cryptoService, messagingService, diff --git a/apps/desktop/src/auth/sso.component.ts b/apps/desktop/src/auth/sso.component.ts index 210319b9ed..cc261f1235 100644 --- a/apps/desktop/src/auth/sso.component.ts +++ b/apps/desktop/src/auth/sso.component.ts @@ -7,6 +7,8 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -39,6 +41,8 @@ export class SsoComponent extends BaseSsoComponent { logService: LogService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, configService: ConfigService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, ) { super( ssoLoginService, @@ -55,6 +59,8 @@ export class SsoComponent extends BaseSsoComponent { logService, userDecryptionOptionsService, configService, + masterPasswordService, + accountService, ); super.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/desktop/src/auth/two-factor.component.ts b/apps/desktop/src/auth/two-factor.component.ts index fdbc52b4bf..d1b84c1fa0 100644 --- a/apps/desktop/src/auth/two-factor.component.ts +++ b/apps/desktop/src/auth/two-factor.component.ts @@ -11,6 +11,8 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -60,6 +62,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, configService: ConfigService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, @Inject(WINDOW) protected win: Window, ) { super( @@ -79,6 +83,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { userDecryptionOptionsService, ssoLoginService, configService, + masterPasswordService, + accountService, ); super.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index b1deba9dd9..afdfc90d76 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Verander Hoofwagwoord" }, - "changeMasterPasswordConfirmation": { - "message": "U kan u hoofwagwoord op die bitwarden.com-webkluis verander. Wil u die webwerf nou besoek?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Vingerafdrukfrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Blaaierintegrasie word nie ondersteun nie" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Ongelukkig word blaaierintegrasie tans slegs in die weergawe vir die Mac-toepwinkel ondersteun." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index b95501bbfd..7869b0894b 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "تغيير كلمة المرور الرئيسية" }, - "changeMasterPasswordConfirmation": { - "message": "يمكنك تغيير كلمة المرور الرئيسية من خزنة الويب في bitwarden.com. هل تريد زيارة الموقع الآن؟" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "عبارة بصمة الإصبع", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "تكامل المتصفح غير مدعوم" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "للأسف، لا يتم دعم تكامل المتصفح إلا في إصدار متجر تطبيقات ماك في الوقت الحالي." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index a18d752620..d4cea4f06e 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -717,7 +717,7 @@ "message": "Bildiriş server URL-si" }, "iconsUrl": { - "message": "Nişan server URL-si" + "message": "İkon server URL-si" }, "environmentSaved": { "message": "Mühit URL-ləri saxlanıldı." @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Ana parolu dəyişdir" }, - "changeMasterPasswordConfirmation": { - "message": "Ana parolunuzu bitwarden.com veb anbarında dəyişdirə bilərsiniz. İndi saytı ziyarət etmək istəyirsiniz?" + "continueToWebApp": { + "message": "Veb tətbiqlə davam edilsin?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Ana parolunuzu Bitwarden veb tətbiqində dəyişdirə bilərsiniz." }, "fingerprintPhrase": { "message": "Barmaq izi ifadəsi", @@ -920,52 +923,52 @@ "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "enableFavicon": { - "message": "Veb sayt nişanlarını göstər" + "message": "Veb sayt ikonlarını göstər" }, "faviconDesc": { "message": "Hər girişin yanında tanına bilən bir təsvir göstər." }, "enableMinToTray": { - "message": "Bildiriş nişanına kiçildin" + "message": "Bildiriş sahəsi ikonuna kiçilt" }, "enableMinToTrayDesc": { - "message": "Pəncərə kiçildiləndə, bildiriş sahəsində bir nişan göstər." + "message": "Pəncərə kiçildiləndə, bunun əvəzinə bildiriş sahəsində bir ikon göstər." }, "enableMinToMenuBar": { - "message": "Menyu sətrinə kiçilt" + "message": "Menyu çubuğuna kiçilt" }, "enableMinToMenuBarDesc": { - "message": "Pəncərəni kiçildəndə, menyu sətrində bir nişan göstər." + "message": "Pəncərəni kiçildəndə, bunun əvəzinə menyu çubuğunda bir ikon göstər." }, "enableCloseToTray": { - "message": "Bildiriş nişanına bağla" + "message": "Bildiriş ikonu üçün bağla" }, "enableCloseToTrayDesc": { - "message": "Pəncərə bağlananda, bildiriş sahəsində bir nişan göstər." + "message": "Pəncərə bağladılanda, bunun əvəzinə bildiriş sahəsində bir ikon göstər." }, "enableCloseToMenuBar": { - "message": "Menyu sətrini bağla" + "message": "Menyu çubuğunda bağla" }, "enableCloseToMenuBarDesc": { - "message": "Pəncərəni bağlananda, menyu sətrində bir nişan göstər." + "message": "Pəncərəni bağladılanda, bunun əvəzinə menyu çubuğunda bir ikon göstər." }, "enableTray": { - "message": "Bildiriş sahəsi nişanını fəallaşdır" + "message": "Bildiriş sahəsi ikonunu göstər" }, "enableTrayDesc": { - "message": "Bildiriş sahəsində həmişə bir nişan göstər." + "message": "Bildiriş sahəsində həmişə bir ikon göstər." }, "startToTray": { - "message": "Bildiriş sahəsi nişanı kimi başlat" + "message": "Bildiriş sahəsi ikonu kimi başlat" }, "startToTrayDesc": { - "message": "Tətbiq ilk başladılanda, yalnız bildiriş sahəsi nişanı görünsün." + "message": "Tətbiq ilk başladılanda, sistem bildiriş sahəsində yalnız ikon olaraq görünsün." }, "startToMenuBar": { - "message": "Menyu sətrini başlat" + "message": "Menyu çubuğunda başlat" }, "startToMenuBarDesc": { - "message": "Tətbiq ilk başladılanda, menyu sətri sadəcə nişan kimi görünsün." + "message": "Tətbiq ilk başladılanda, menyu çubuğunda yalnız ikon olaraq görünsün." }, "openAtLogin": { "message": "Giriş ediləndə avtomatik başlat" @@ -977,7 +980,7 @@ "message": "\"Dock\"da həmişə göstər" }, "alwaysShowDockDesc": { - "message": "Menyu sətrinə kiçildiləndə belə Bitwarden nişanını \"Dock\"da göstər." + "message": "Menyu çubuğuna kiçildiləndə belə Bitwarden ikonunu Yuvada göstər." }, "confirmTrayTitle": { "message": "Bildiriş sahəsi nişanını ləğv et" @@ -1450,16 +1453,16 @@ "message": "Hesabınız bağlandı və bütün əlaqəli datalar silindi." }, "preferences": { - "message": "Tercihlər" + "message": "Tərcihlər" }, "enableMenuBar": { - "message": "Menyu sətri nişanını fəallaşdır" + "message": "Menyu çubuğu ikonunu göstər" }, "enableMenuBarDesc": { - "message": "Menyu sətrində həmişə bir nişan göstər." + "message": "Menyu çubuğunda həmişə bir ikon göstər." }, "hideToMenuBar": { - "message": "Menyu sətrini gizlət" + "message": "Menyu çubuğunda gizlət" }, "selectOneCollection": { "message": "Ən azı bir kolleksiya seçməlisiniz." @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Brauzer inteqrasiyası dəstəklənmir" }, + "browserIntegrationErrorTitle": { + "message": "Brauzer inteqrasiyasını fəallaşdırma xətası" + }, + "browserIntegrationErrorDesc": { + "message": "Brauzer inteqrasiyasını fəallaşdırarkən bir xəta baş verdi." + }, "browserIntegrationMasOnlyDesc": { "message": "Təəssüf ki, brauzer inteqrasiyası indilik yalnız Mac App Store versiyasında dəstəklənir." }, @@ -2021,7 +2030,7 @@ "message": "Eyni vaxtda 5-dən çox hesaba giriş edilə bilməz." }, "accountPreferences": { - "message": "Tercihlər" + "message": "Tərcihlər" }, "appPreferences": { "message": "Tətbiq ayarları (bütün hesablar)" @@ -2689,12 +2698,24 @@ "description": "Label indicating the most common import formats" }, "troubleshooting": { - "message": "Troubleshooting" + "message": "Problemlərin aradan qaldırılması" }, "disableHardwareAccelerationRestart": { - "message": "Disable hardware acceleration and restart" + "message": "Avadanlıq sürətləndirməni ləğv et və yenidən başlat" }, "enableHardwareAccelerationRestart": { - "message": "Enable hardware acceleration and restart" + "message": "Avadanlıq sürətləndirməni işə sal və yenidən başlat" + }, + "removePasskey": { + "message": "Parolu sil" + }, + "passkeyRemoved": { + "message": "Parol silindi" + }, + "errorAssigningTargetCollection": { + "message": "Hədəf kolleksiyaya təyin etmə xətası." + }, + "errorAssigningTargetFolder": { + "message": "Hədəf qovluğa təyin etmə xətası." } } diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index 0529c407a4..53e3ec2d12 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Змяніць асноўны пароль" }, - "changeMasterPasswordConfirmation": { - "message": "Вы можаце змяніць свой асноўны пароль у вэб-сховішчы на bitwarden.com. Перайсці на вэб-сайт зараз?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Фраза адбітка пальца", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Інтэграцыя з браўзерам не падтрымліваецца" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "На жаль, інтэграцыя з браўзерам зараз падтрымліваецца толькі ў версіі для Mac App Store." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 3217f167ed..f4886c420f 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Промяна на главната парола" }, - "changeMasterPasswordConfirmation": { - "message": "Главната парола на трезор може да се промени чрез сайта bitwarden.com. Искате ли да го посетите?" + "continueToWebApp": { + "message": "Продължаване към уеб приложението?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Може да промените главната си парола в уеб приложението на Битуорден." }, "fingerprintPhrase": { "message": "Уникална фраза", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Интеграцията с браузър не се поддържа" }, + "browserIntegrationErrorTitle": { + "message": "Грешка при включването на интеграцията с браузъра" + }, + "browserIntegrationErrorDesc": { + "message": "Възникна грешка при включването на интеграцията с браузъра." + }, "browserIntegrationMasOnlyDesc": { "message": "За жалост в момента интеграцията с браузър не се поддържа във версията за магазина на Mac." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Включете хардуерното ускорение и рестартирайте" + }, + "removePasskey": { + "message": "Премахване на секретния ключ" + }, + "passkeyRemoved": { + "message": "Секретният ключ е премахнат" + }, + "errorAssigningTargetCollection": { + "message": "Грешка при задаването на целева колекция." + }, + "errorAssigningTargetFolder": { + "message": "Грешка при задаването на целева папка." } } diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 9599fc6827..abd2c1cfae 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "মূল পাসওয়ার্ড পরিবর্তন" }, - "changeMasterPasswordConfirmation": { - "message": "আপনি bitwarden.com ওয়েব ভল্ট থেকে মূল পাসওয়ার্ডটি পরিবর্তন করতে পারেন। আপনি কি এখনই ওয়েবসাইটটি দেখতে চান?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "ফিঙ্গারপ্রিন্ট ফ্রেজ", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 6e6ca99bac..825bd6344e 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Promijenite glavnu lozinku" }, - "changeMasterPasswordConfirmation": { - "message": "Možete da promjenite svoju glavnu lozinku na bitwarden.com web trezoru. Da li želite da posjetite web stranicu sada?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Jedinstvena fraza", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Nažalost, za sada je integracija sa preglednikom podržana samo u Mac App Store verziji aplikacije." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 3cdedd0274..6c48d6cb0b 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Canvia la contrasenya mestra" }, - "changeMasterPasswordConfirmation": { - "message": "Podeu canviar la contrasenya mestra a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Frase d'empremta digital", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "La integració en el navegador no és compatible" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Malauradament, la integració del navegador només és compatible amb la versió de Mac App Store." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Activeu l'acceleració i reinicieu el maquinari" + }, + "removePasskey": { + "message": "Suprimeix la clau de pas" + }, + "passkeyRemoved": { + "message": "Clau de pas suprimida" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index efa1dccdc1..550b10a31c 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Změnit hlavní heslo" }, - "changeMasterPasswordConfirmation": { - "message": "Hlavní heslo si můžete změnit na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít?" + "continueToWebApp": { + "message": "Pokračovat do webové aplikace?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hlavní heslo můžete změnit ve webové aplikaci Bitwardenu." }, "fingerprintPhrase": { "message": "Fráze otisku prstu", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integrace prohlížeče není podporována" }, + "browserIntegrationErrorTitle": { + "message": "Chyba při povolování integrace prohlížeče" + }, + "browserIntegrationErrorDesc": { + "message": "Vyskytla se chyba při povolování integrace prohlížeče." + }, "browserIntegrationMasOnlyDesc": { "message": "Integrace prohlížeče je podporována jen ve verzi pro Mac App Store." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Povolit hardwarovou akceleraci a restartovat" + }, + "removePasskey": { + "message": "Odebrat přístupový klíč" + }, + "passkeyRemoved": { + "message": "Přístupový klíč byl odebrán" + }, + "errorAssigningTargetCollection": { + "message": "Chyba při přiřazování cílové kolekce." + }, + "errorAssigningTargetFolder": { + "message": "Chyba při přiřazování cílové složky." } } diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 65ce77b340..b1cc9e63d3 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 0da6c705ca..f2a84a3c29 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Skift hovedadgangskode" }, - "changeMasterPasswordConfirmation": { - "message": "Man kan ændre sin hovedadgangskode via bitwarden.com web-boksen. Besøg webstedet nu?" + "continueToWebApp": { + "message": "Fortsæt til web-app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hovedadgangskoden kan ændres via Bitwarden web-appen." }, "fingerprintPhrase": { "message": "Fingeraftrykssætning", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browserintegration understøttes ikke" }, + "browserIntegrationErrorTitle": { + "message": "Fejl ved aktivering af webbrowserintegration" + }, + "browserIntegrationErrorDesc": { + "message": "En fejl opstod under aktivering af webbrowserintegration." + }, "browserIntegrationMasOnlyDesc": { "message": "Desværre understøttes browserintegration indtil videre kun i Mac App Store-versionen." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Aktivér hardwareacceleration og genstart" + }, + "removePasskey": { + "message": "Fjern adgangsnøgle" + }, + "passkeyRemoved": { + "message": "Adgangsnøgle fjernet" + }, + "errorAssigningTargetCollection": { + "message": "Fejl ved tildeling af målsamling." + }, + "errorAssigningTargetFolder": { + "message": "Fejl ved tildeling af målmappe." } } diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index bacf158023..e5e3945abc 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Master-Passwort ändern" }, - "changeMasterPasswordConfirmation": { - "message": "Du kannst dein Master-Passwort im bitwarden.com-Web-Tresor ändern. Möchtest du die Seite jetzt öffnen?" + "continueToWebApp": { + "message": "Weiter zur Web-App?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Du kannst dein Master-Passwort in der Bitwarden Web-App ändern." }, "fingerprintPhrase": { "message": "Fingerabdruck-Phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser-Integration wird nicht unterstützt" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Leider wird die Browser-Integration derzeit nur in der Mac App Store Version unterstützt." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Hardwarebeschleunigung aktivieren und neu starten" + }, + "removePasskey": { + "message": "Passkey entfernen" + }, + "passkeyRemoved": { + "message": "Passkey entfernt" + }, + "errorAssigningTargetCollection": { + "message": "Fehler beim Zuweisen der Ziel-Sammlung." + }, + "errorAssigningTargetFolder": { + "message": "Fehler beim Zuweisen des Ziel-Ordners." } } diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index f5e18bdb85..41e6a62a2f 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Αλλαγή Κύριου Κωδικού" }, - "changeMasterPasswordConfirmation": { - "message": "Μπορείτε να αλλάξετε τον κύριο κωδικό στο bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Φράση Δακτυλικών Αποτυπωμάτων", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Η ενσωμάτωση του περιηγητή δεν υποστηρίζεται" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Δυστυχώς η ενσωμάτωση του προγράμματος περιήγησης υποστηρίζεται μόνο στην έκδοση Mac App Store για τώρα." }, @@ -2550,22 +2559,22 @@ "message": "Σφάλμα αποκρυπτογράφησης του εξαγόμενου αρχείου. Το κλειδί κρυπτογράφησης δεν ταιριάζει με το κλειδί κρυπτογράφησης που χρησιμοποιήθηκε για την εξαγωγή των δεδομένων." }, "invalidFilePassword": { - "message": "Invalid file password, please use the password you entered when you created the export file." + "message": "Μη έγκυρος κωδικός πρόσβασης, παρακαλώ χρησιμοποιήστε τον κωδικό πρόσβασης που εισαγάγατε όταν δημιουργήσατε το αρχείο εξαγωγής." }, "importDestination": { - "message": "Import destination" + "message": "Προορισμός εισαγωγής" }, "learnAboutImportOptions": { - "message": "Learn about your import options" + "message": "Μάθετε για τις επιλογές εισαγωγής σας" }, "selectImportFolder": { - "message": "Select a folder" + "message": "Επιλέξτε ένα φάκελο" }, "selectImportCollection": { - "message": "Select a collection" + "message": "Επιλέξτε μια συλλογή" }, "importTargetHint": { - "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", + "message": "Επιλέξτε αυτό αν θέλετε τα περιεχόμενα του εισαγόμενου αρχείου να μετακινηθούν σε $DESTINATION$", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -2575,25 +2584,25 @@ } }, "importUnassignedItemsError": { - "message": "File contains unassigned items." + "message": "Το αρχείο περιέχει μη συσχετισμένα στοιχεία." }, "selectFormat": { - "message": "Select the format of the import file" + "message": "Επιλέξτε τη μορφή του αρχείου εισαγωγής" }, "selectImportFile": { - "message": "Select the import file" + "message": "Επιλέξτε το αρχείο εισαγωγής" }, "chooseFile": { - "message": "Choose File" + "message": "Επιλογή Αρχείου" }, "noFileChosen": { - "message": "No file chosen" + "message": "Δεν επιλέχθηκε κανένα αρχείο" }, "orCopyPasteFileContents": { - "message": "or copy/paste the import file contents" + "message": "ή αντιγράψτε/επικολλήστε τα περιεχόμενα του αρχείου εισαγωγής" }, "instructionsFor": { - "message": "$NAME$ Instructions", + "message": "$NAME$ Οδηγίες", "description": "The title for the import tool instructions.", "placeholders": { "name": { @@ -2603,34 +2612,34 @@ } }, "confirmVaultImport": { - "message": "Confirm vault import" + "message": "Επιβεβαίωση εισαγωγής κρύπτης" }, "confirmVaultImportDesc": { - "message": "This file is password-protected. Please enter the file password to import data." + "message": "Αυτό το αρχείο προστατεύεται με κωδικό πρόσβασης. Παρακαλώ εισαγάγετε τον κωδικό πρόσβασης για την εισαγωγή δεδομένων." }, "confirmFilePassword": { - "message": "Confirm file password" + "message": "Επιβεβαίωση κωδικού πρόσβασης αρχείου" }, "multifactorAuthenticationCancelled": { - "message": "Multifactor authentication cancelled" + "message": "Ο πολυμερής έλεγχος ταυτότητας ακυρώθηκε" }, "noLastPassDataFound": { - "message": "No LastPass data found" + "message": "Δεν βρέθηκαν δεδομένα LastPass" }, "incorrectUsernameOrPassword": { - "message": "Incorrect username or password" + "message": "Λάθος όνομα χρήστη ή κωδικού πρόσβασης" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Λάθος κωδικός" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Λάθος κωδικός" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "Λάθος PIN" }, "multifactorAuthenticationFailed": { - "message": "Multifactor authentication failed" + "message": "Ο πολυμερής έλεγχος ταυτότητας απέτυχε" }, "includeSharedFolders": { "message": "Συμπερίληψη κοινόχρηστων φακέλων" @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Ενεργοποίηση επιτάχυνσης υλικού και επανεκκίνηση" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 394a5951e9..ff9cbc97cc 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 9b68b6de49..2658610df3 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 3ffc46eba1..0542da9ddc 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index dcffc08ea4..1c4cc4f0be 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index 29c345d235..ec5da44293 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Cambiar contraseña maestra" }, - "changeMasterPasswordConfirmation": { - "message": "Puedes cambiar tu contraseña maestra en la caja fuerte web de bitwarden.com. ¿Quieres visitar ahora el sitio web?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Frase de huella digital", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "La integración con el navegador no está soportada" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Por desgracia la integración del navegador sólo está soportada por ahora en la versión de la Mac App Store." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 663cd873e5..3850cc1d85 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Muuda ülemparooli" }, - "changeMasterPasswordConfirmation": { - "message": "Saad oma ülemparooli muuta bitwarden.com veebihoidlas. Soovid seda kohe teha?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Sõrmejälje fraas", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Brauseri integratsioon ei ole toetatud" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Paraku on brauseri integratsioon hetkel toetatud ainult Mac App Store'i versioonis." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index 8970af1350..b21108b6ad 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Aldatu pasahitz nagusia" }, - "changeMasterPasswordConfirmation": { - "message": "Zure pasahitz nagusia alda dezakezu bitwarden.com webgunean. Orain joan nahi duzu webgunera?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Hatz-marka digitalaren esaldia", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Ez da nabigatzailearen integrazioa onartzen" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Zoritxarrez, Mac App Storeren bertsioan soilik onartzen da oraingoz nabigatzailearen integrazioa." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index a8a5758a14..08356d410d 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "تغییر کلمه عبور اصلی" }, - "changeMasterPasswordConfirmation": { - "message": "شما می‌توانید کلمه عبور اصلی خود را در bitwarden.com تغییر دهید. آیا می‌خواهید از سایت بازدید کنید؟" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "عبارت اثر انگشت", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "ادغام مرورگر پشتیبانی نمی‌شود" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "متأسفانه در حال حاضر ادغام مرورگر فقط در نسخه Mac App Store پشتیبانی می‌شود." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index 5c069578fa..fca24c197a 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -494,7 +494,7 @@ "message": "Kansio poistettiin" }, "loginOrCreateNewAccount": { - "message": "Käytä salattua holviasi kirjautumalla sisään tai tai luo uusi tili." + "message": "Käytä salattua holviasi kirjautumalla sisään tai luo uusi tili." }, "createAccount": { "message": "Luo tili" @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Vaihda pääsalasana" }, - "changeMasterPasswordConfirmation": { - "message": "Voit vaihtaa pääsalasanasi bitwarden.com-verkkoholvissa. Haluatko käydä sivustolla nyt?" + "continueToWebApp": { + "message": "Avataanko verkkosovellus?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Voit vaihtaa pääsalasanasi Bitwardenin verkkosovelluksessa." }, "fingerprintPhrase": { "message": "Tunnistelauseke", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Selainintegraatiota ei tueta" }, + "browserIntegrationErrorTitle": { + "message": "Virhe otettaessa selainintegrointia käyttöön" + }, + "browserIntegrationErrorDesc": { + "message": "Otettaessa selainintegraatiota käyttöön tapahtui virhe." + }, "browserIntegrationMasOnlyDesc": { "message": "Valitettavasti selainintegraatiota tuetaan toistaiseksi vain Mac App Store -versiossa." }, @@ -2689,12 +2698,24 @@ "description": "Label indicating the most common import formats" }, "troubleshooting": { - "message": "Troubleshooting" + "message": "Vianetsintä" }, "disableHardwareAccelerationRestart": { - "message": "Disable hardware acceleration and restart" + "message": "Poista laitteistokiihdytys käytöstä ja käynnistä sovellus uudelleen" }, "enableHardwareAccelerationRestart": { - "message": "Enable hardware acceleration and restart" + "message": "Ota laitteistokiihdytys käyttöön ja käynnistä sovellus uudelleen" + }, + "removePasskey": { + "message": "Poista suojausavain" + }, + "passkeyRemoved": { + "message": "Suojausavain poistettiin" + }, + "errorAssigningTargetCollection": { + "message": "Virhe määritettäessä kohdekokoelmaa." + }, + "errorAssigningTargetFolder": { + "message": "Virhe määritettäessä kohdekansiota." } } diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 6e6d0abaa6..6d5f85fca8 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Palitan ang master password" }, - "changeMasterPasswordConfirmation": { - "message": "Maaari mong palitan ang iyong master password sa bitwarden.com web vault. Gusto mo bang bisitahin ang website ngayon?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Hulmabig ng Hilik ng Dako", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Hindi suportado ang pagsasama ng browser" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Sa kasamaang palad ang pagsasama ng browser ay suportado lamang sa bersyon ng Mac App Store para sa ngayon." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 90dc4b0f77..1097624b14 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Changer le mot de passe principal" }, - "changeMasterPasswordConfirmation": { - "message": "Vous pouvez changer votre mot de passe principal depuis le coffre web de bitwarden.com. Voulez-vous visiter le site web maintenant ?" + "continueToWebApp": { + "message": "Poursuivre vers l'application web ?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Vous pouvez modifier votre mot de passe principal sur l'application web de Bitwarden." }, "fingerprintPhrase": { "message": "Phrase d'empreinte", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Intégration dans le navigateur non supportée" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Malheureusement l'intégration avec le navigateur est uniquement supportée dans la version Mac App Store pour le moment." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Activer l'accélération matérielle et redémarrer" + }, + "removePasskey": { + "message": "Retirer la clé d'identification (passkey)" + }, + "passkeyRemoved": { + "message": "Clé d'identification (passkey) retirée" + }, + "errorAssigningTargetCollection": { + "message": "Erreur lors de l'assignation de la collection cible." + }, + "errorAssigningTargetFolder": { + "message": "Erreur lors de l'assignation du dossier cible." } } diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 11694b8c9c..90648699c0 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 3a9517e8d3..73599c012e 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "החלף סיסמה ראשית" }, - "changeMasterPasswordConfirmation": { - "message": "באפשרותך לשנות את הסיסמה הראשית שלך דרך הכספת באתר bitwarden.com. האם ברצונך לפתוח את האתר כעת?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "סיסמת טביעת אצבע", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "שילוב הדפדפן אינו נתמך" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "למצער, אינטגרצייה עם הדפדפן בשלב זה נתמכת רק על ידי גרסת חנות האפליקציות של מקינטוש." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 86f17c1c83..4f8bb9b4bb 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 5af3644eea..220c8bfab2 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Promjeni glavnu lozinku" }, - "changeMasterPasswordConfirmation": { - "message": "Svoju glavnu lozinku možeš promijeniti na web trezoru. Želiš li sada posjetiti bitwarden.com?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Jedinstvena fraza", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integracija preglednika nije podržana" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Nažalost, za sada je integracija s preglednikom podržana samo u Mac App Store verziji aplikacije." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 0b61e5f675..149d48284e 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Mesterjelszó módosítása" }, - "changeMasterPasswordConfirmation": { - "message": "A mesterjelszó megváltoztatható a bitwarden.com webes széfben. Szeretnénk felkeresni a webhelyet mos?" + "continueToWebApp": { + "message": "Tovább a webes alkalmazáshoz?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "A mesterjelszó a Bitwarden webalkalmazásban módosítható." }, "fingerprintPhrase": { "message": "Azonosítókifejezés", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "A böngésző integráció nem támogatott." }, + "browserIntegrationErrorTitle": { + "message": "Böngésző integráció engedélyezése" + }, + "browserIntegrationErrorDesc": { + "message": "Hiba történt a böngésző integrációjának engedélyezése közben." + }, "browserIntegrationMasOnlyDesc": { "message": "Sajnos a böngésző integrációt egyelőre csak a Mac App Store verzió támogatja." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "A hardveres gyorsítás engedélyezése és újraindítás" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Hiba történt a célgyűjtemény hozzárendelése során." + }, + "errorAssigningTargetFolder": { + "message": "Hiba történt a célmappa hozzárendelése során." } } diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 5290fd11de..3194b0f7d3 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -404,7 +404,7 @@ "message": "Panjang" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "Panjang kata sandi minimum" }, "uppercase": { "message": "Huruf Kapital (A-Z)" @@ -545,7 +545,7 @@ "message": "Diperlukan pengetikan ulang kata sandi utama." }, "masterPasswordMinlength": { - "message": "Master password must be at least $VALUE$ characters long.", + "message": "Kata sandi utama minimal harus $VALUE$ karakter.", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -561,7 +561,7 @@ "message": "Akun baru Anda telah dibuat! Sekarang Anda bisa masuk." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Anda berhasil masuk" }, "youMayCloseThisWindow": { "message": "You may close this window" @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Ubah Kata Sandi Utama" }, - "changeMasterPasswordConfirmation": { - "message": "Anda dapat mengubah kata sandi utama Anda di brankas web bitwarden.com. Anda ingin mengunjungi situs web sekarang?" + "continueToWebApp": { + "message": "Lanjutkan ke aplikasi web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Anda bisa mengganti kata sandi utama Anda di aplikasi web Bitwarden." }, "fingerprintPhrase": { "message": "Frase Fingerprint", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integrasi browser tidak didukung" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Sayangnya integrasi browser hanya didukung di versi Mac App Store untuk saat ini." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index 2736a4a46a..08ae2d9da8 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Cambia password principale" }, - "changeMasterPasswordConfirmation": { - "message": "Puoi cambiare la tua password principale sulla cassaforte online di bitwarden.com. Vuoi visitare ora il sito?" + "continueToWebApp": { + "message": "Passa al sito web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Puoi modificare la tua password principale sul sito web di Bitwarden." }, "fingerprintPhrase": { "message": "Frase impronta", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "L'integrazione del browser non è supportata" }, + "browserIntegrationErrorTitle": { + "message": "Errore durante l'attivazione dell'integrazione del browser" + }, + "browserIntegrationErrorDesc": { + "message": "Si è verificato un errore durante l'attivazione dell'integrazione del browser." + }, "browserIntegrationMasOnlyDesc": { "message": "Purtroppo l'integrazione del browser è supportata solo nella versione nell'App Store per ora." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Attiva l'accelerazione hardware e riavvia" + }, + "removePasskey": { + "message": "Rimuovi passkey" + }, + "passkeyRemoved": { + "message": "Passkey rimossa" + }, + "errorAssigningTargetCollection": { + "message": "Errore nell'assegnazione della raccolta di destinazione." + }, + "errorAssigningTargetFolder": { + "message": "Errore nell'assegnazione della cartella di destinazione." } } diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index a4b213a5fd..b07eab20cc 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "マスターパスワードの変更" }, - "changeMasterPasswordConfirmation": { - "message": "マスターパスワードは bitwarden.com ウェブ保管庫で変更できます。ウェブサイトを開きますか?" + "continueToWebApp": { + "message": "ウェブアプリに進みますか?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Bitwarden ウェブアプリでマスターパスワードを変更できます。" }, "fingerprintPhrase": { "message": "パスフレーズ", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "ブラウザー統合はサポートされていません" }, + "browserIntegrationErrorTitle": { + "message": "ブラウザー連携を有効にする際にエラーが発生しました" + }, + "browserIntegrationErrorDesc": { + "message": "ブラウザー統合の有効化中にエラーが発生しました。" + }, "browserIntegrationMasOnlyDesc": { "message": "残念ながら、ブラウザ統合は、Mac App Storeのバージョンでのみサポートされています。" }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "ハードウェアアクセラレーションを有効にして再起動する" + }, + "removePasskey": { + "message": "パスキーを削除" + }, + "passkeyRemoved": { + "message": "パスキーを削除しました" + }, + "errorAssigningTargetCollection": { + "message": "ターゲットコレクションの割り当てに失敗しました。" + }, + "errorAssigningTargetFolder": { + "message": "ターゲットフォルダーの割り当てに失敗しました。" } } diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 11694b8c9c..90648699c0 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 11694b8c9c..90648699c0 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 5f7ad11dcd..162cba3a75 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "ಮಾಸ್ಟರ್ ಪಾಸ್ವರ್ಡ್ ಬದಲಾಯಿಸಿ" }, - "changeMasterPasswordConfirmation": { - "message": "ನಿಮ್ಮ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ನೀವು bitwarden.com ವೆಬ್ ವಾಲ್ಟ್‌ನಲ್ಲಿ ಬದಲಾಯಿಸಬಹುದು. ನೀವು ಈಗ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಭೇಟಿ ನೀಡಲು ಬಯಸುವಿರಾ?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "ಫಿಂಗರ್ಪ್ರಿಂಟ್ ಫ್ರೇಸ್", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "ದುರದೃಷ್ಟವಶಾತ್ ಬ್ರೌಸರ್ ಏಕೀಕರಣವನ್ನು ಇದೀಗ ಮ್ಯಾಕ್ ಆಪ್ ಸ್ಟೋರ್ ಆವೃತ್ತಿಯಲ್ಲಿ ಮಾತ್ರ ಬೆಂಬಲಿಸಲಾಗುತ್ತದೆ." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index c8e6719811..09b1767af0 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "마스터 비밀번호 변경" }, - "changeMasterPasswordConfirmation": { - "message": "bitwarden.com 웹 보관함에서 마스터 비밀번호를 바꿀 수 있습니다. 지금 웹 사이트를 방문하시겠습니까?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "지문 구절", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "브라우저와 연결이 지원되지 않음" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "브라우저와 연결은 현재 Mac App Store 버전에서만 지원됩니다." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index 00186771fd..de77aa8fbf 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Keisti pagrindinį slaptažodį" }, - "changeMasterPasswordConfirmation": { - "message": "Pagrindinį slaptažodį galite pakeisti bitwarden.com žiniatinklio saugykloje. Ar norite dabar apsilankyti svetainėje?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Piršto antspaudo frazė", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Naršyklės integravimas nepalaikomas" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Deja, bet naršyklės integravimas kol kas palaikomas tik Mac App Store versijoje." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index f82b762d6c..521a0afcf2 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Mainīt galveno paroli" }, - "changeMasterPasswordConfirmation": { - "message": "Galveno paroli ir iespējams mainīt bitwarden.com tīmekļa glabātavā. Vai tagad apmeklēt tīmekļvietni?" + "continueToWebApp": { + "message": "Pāriet uz tīmekļa lietotni?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Savu galveno paroli var mainīt Bitwarden tīmekļa lietotnē." }, "fingerprintPhrase": { "message": "Atpazīšanas vārdkopa", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Sasaistīšana ar pārlūku nav atbalstīta" }, + "browserIntegrationErrorTitle": { + "message": "Kļūda pārlūga saistīšanas iespējošanā" + }, + "browserIntegrationErrorDesc": { + "message": "Atgadījās kļūda pārlūka saistīšanas iespējošanas laikā." + }, "browserIntegrationMasOnlyDesc": { "message": "Diemžēl sasaistīšāna ar pārlūku pagaidām ir nodrošināta tikai Mac App Store laidienā." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Iespējot aparatūras paātrinājumu un pārsāknēt" + }, + "removePasskey": { + "message": "Noņemt piekļuves atslēgu" + }, + "passkeyRemoved": { + "message": "Piekļuves atslēga noņemta" + }, + "errorAssigningTargetCollection": { + "message": "Kļūda mērķa krājuma piešķiršanā." + }, + "errorAssigningTargetFolder": { + "message": "Kļūda mērķa mapes piešķiršanā." } } diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 533ec2ebba..ed458379b8 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Promjena glavne lozinke" }, - "changeMasterPasswordConfirmation": { - "message": "Možete promijeniti svoju glavnu lozinku u trezoru na internet strani bitwarden.com. Da li želite da posjetite internet lokaciju sada?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fraza računa", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index cba807216a..b94b9d1b79 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "പ്രാഥമിക പാസ്‌വേഡ് മാറ്റുക" }, - "changeMasterPasswordConfirmation": { - "message": "തങ്ങൾക്കു Bitwarden വെബ് വാൾട്ടിൽ പ്രാഥമിക പാസ്‌വേഡ് മാറ്റാൻ സാധിക്കും.വെബ്സൈറ്റ് ഇപ്പോൾ സന്ദർശിക്കാൻ ആഗ്രഹിക്കുന്നുവോ?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "ഫിംഗർപ്രിന്റ് ഫ്രേസ്‌", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 11694b8c9c..90648699c0 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index 05e3e7703e..5142c8e61f 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index a20a6a6267..e190cfc236 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Endre hovedpassordet" }, - "changeMasterPasswordConfirmation": { - "message": "Du kan endre superpassordet ditt på bitwarden.net-netthvelvet. Vil du besøke det nettstedet nå?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingeravtrykksfrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Nettleserintegrasjon støttes ikke" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Nettleserintegrasjon støttes dessverre bare i Mac App Store-versjonen for øyeblikket." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 40e3d11d88..bd58a18b0d 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 0ae432ef45..3ca0730710 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Hoofdwachtwoord wijzigen" }, - "changeMasterPasswordConfirmation": { - "message": "Je kunt je hoofdwachtwoord wijzigen in de kluis op bitwarden.com. Wil je de website nu bezoeken?" + "continueToWebApp": { + "message": "Doorgaan naar web-app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Je kunt je hoofdwachtwoord wijzigen in de Bitwarden-webapp." }, "fingerprintPhrase": { "message": "Vingerafdrukzin", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browserintegratie niet ondersteund" }, + "browserIntegrationErrorTitle": { + "message": "Fout bij inschakelen van de browserintegratie" + }, + "browserIntegrationErrorDesc": { + "message": "Er is iets misgegaan bij het tijdens het inschakelen van de browserintegratie." + }, "browserIntegrationMasOnlyDesc": { "message": "Helaas wordt browserintegratie momenteel alleen ondersteund in de Mac App Store-versie." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Hardwareversnelling inschakelen en herstarten" + }, + "removePasskey": { + "message": "Passkey verwijderen" + }, + "passkeyRemoved": { + "message": "Passkey verwijderd" + }, + "errorAssigningTargetCollection": { + "message": "Fout bij toewijzen doelverzameling." + }, + "errorAssigningTargetFolder": { + "message": "Fout bij toewijzen doelmap." } } diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index d3119f6a10..12e11b32c1 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Endre hovudpassord" }, - "changeMasterPasswordConfirmation": { - "message": "Du kan endre hovudpassordet ditt i Bitwarden sin nettkvelv. Vil du gå til nettstaden no?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index 20b704ef5e..7363558551 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 60444211be..df7a158a3a 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Zmień hasło główne" }, - "changeMasterPasswordConfirmation": { - "message": "Hasło główne możesz zmienić na stronie sejfu bitwarden.com. Czy chcesz przejść do tej strony?" + "continueToWebApp": { + "message": "Kontynuować do aplikacji internetowej?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Możesz zmienić swoje hasło główne w aplikacji internetowej Bitwarden." }, "fingerprintPhrase": { "message": "Unikalny identyfikator konta", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Połączenie z przeglądarką nie jest obsługiwane" }, + "browserIntegrationErrorTitle": { + "message": "Błąd podczas włączania integracji z przeglądarką" + }, + "browserIntegrationErrorDesc": { + "message": "Wystąpił błąd podczas włączania integracji z przeglądarką." + }, "browserIntegrationMasOnlyDesc": { "message": "Połączenie z przeglądarką jest obsługiwane tylko z wersją aplikacji ze sklepu Mac App Store." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Włącz akcelerację sprzętową i uruchom ponownie" + }, + "removePasskey": { + "message": "Usuń passkey" + }, + "passkeyRemoved": { + "message": "Passkey został usunięty" + }, + "errorAssigningTargetCollection": { + "message": "Wystąpił błąd podczas przypisywania kolekcji." + }, + "errorAssigningTargetFolder": { + "message": "Wystąpił błąd podczas przypisywania folderu." } } diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 4fb7c74889..f651e0b060 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Alterar Senha Mestra" }, - "changeMasterPasswordConfirmation": { - "message": "Você pode alterar a sua senha mestra no cofre web em bitwarden.com. Você deseja visitar o site agora?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Frase biométrica", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integração com o navegador não suportado" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Infelizmente, por ora, a integração do navegador só é suportada na versão da Mac App Store." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 7c325bc5f9..4e58dad2cf 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Alterar palavra-passe mestra" }, - "changeMasterPasswordConfirmation": { - "message": "Pode alterar o seu endereço de e-mail no cofre do site bitwarden.com. Deseja visitar o site agora?" + "continueToWebApp": { + "message": "Continuar para a aplicação Web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Pode alterar a sua palavra-passe mestra na aplicação Web Bitwarden." }, "fingerprintPhrase": { "message": "Frase de impressão digital", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integração com o navegador não suportada" }, + "browserIntegrationErrorTitle": { + "message": "Erro ao ativar a integração do navegador" + }, + "browserIntegrationErrorDesc": { + "message": "Ocorreu um erro ao ativar a integração do navegador." + }, "browserIntegrationMasOnlyDesc": { "message": "Infelizmente, a integração do navegador só é suportada na versão da Mac App Store por enquanto." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Ativar a aceleração de hardware e reiniciar" + }, + "removePasskey": { + "message": "Remover chave de acesso" + }, + "passkeyRemoved": { + "message": "Chave de acesso removida" + }, + "errorAssigningTargetCollection": { + "message": "Erro ao atribuir a coleção de destino." + }, + "errorAssigningTargetFolder": { + "message": "Erro ao atribuir a pasta de destino." } } diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 082a4f590b..3fe73f28a7 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Schimbare parolă principală" }, - "changeMasterPasswordConfirmation": { - "message": "Puteți modifica parola principală pe saitul web bitwarden.com. Doriți să vizitați saitul acum?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Frază amprentă", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integrarea browserului nu este acceptată" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Din păcate, integrarea browserului este acceptată numai în versiunea Mac App Store pentru moment." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 5eaad37d0f..cc182812a6 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Изменить мастер-пароль" }, - "changeMasterPasswordConfirmation": { - "message": "Вы можете изменить свой мастер-пароль на bitwarden.com. Перейти на сайт сейчас?" + "continueToWebApp": { + "message": "Перейти к веб-приложению?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Изменить мастер-пароль можно в веб-приложении Bitwarden." }, "fingerprintPhrase": { "message": "Фраза отпечатка", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Интеграция с браузером не поддерживается" }, + "browserIntegrationErrorTitle": { + "message": "Ошибка при включении интеграции с браузером" + }, + "browserIntegrationErrorDesc": { + "message": "Произошла ошибка при включении интеграции с браузером." + }, "browserIntegrationMasOnlyDesc": { "message": "К сожалению, интеграция браузера пока поддерживается только в версии Mac App Store." }, @@ -2480,13 +2489,13 @@ "message": "Перейти к содержимому" }, "typePasskey": { - "message": "Ключ доступа" + "message": "Passkey" }, "passkeyNotCopied": { - "message": "Ключ доступа не будет скопирован" + "message": "Passkey не будет скопирован" }, "passkeyNotCopiedAlert": { - "message": "Ключ доступа не будет скопирован в клонированный элемент. Продолжить клонирование этого элемента?" + "message": "Passkey не будет скопирован в клонированный элемент. Продолжить клонирование этого элемента?" }, "aliasDomain": { "message": "Псевдоним домена" @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Включить аппаратное ускорение и перезапустить" + }, + "removePasskey": { + "message": "Удалить passkey" + }, + "passkeyRemoved": { + "message": "Passkey удален" + }, + "errorAssigningTargetCollection": { + "message": "Ошибка при назначении целевой коллекции." + }, + "errorAssigningTargetFolder": { + "message": "Ошибка при назначении целевой папки." } } diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index c72b2dd0e6..261ae1c9b8 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index b8777adeb1..6ef52a83ee 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -445,7 +445,7 @@ "message": "Vyhnúť sa zameniteľným znakom" }, "searchCollection": { - "message": "Search collection" + "message": "Vyhľadať zbierku" }, "searchFolder": { "message": "Search folder" @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Zmeniť hlavné heslo" }, - "changeMasterPasswordConfirmation": { - "message": "Svoje hlavné heslo môžete zmeniť vo webovom trezore bitwarden.com. Chcete teraz navštíviť túto stránku?" + "continueToWebApp": { + "message": "Pokračovať vo webovej aplikácii?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hlavné heslo si môžete zmeniť vo webovej aplikácii Bitwarden." }, "fingerprintPhrase": { "message": "Fráza odtlačku", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integrácia v prehliadači nie je podporovaná" }, + "browserIntegrationErrorTitle": { + "message": "Chyba pri povoľovaní integrácie v prehliadači" + }, + "browserIntegrationErrorDesc": { + "message": "Pri povoľovaní integrácie v prehliadači sa vyskytla chyba." + }, "browserIntegrationMasOnlyDesc": { "message": "Bohužiaľ, integrácia v prehliadači je zatiaľ podporovaná iba vo verzii Mac App Store." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Povoliť hardvérové zrýchlenie a reštartovať" + }, + "removePasskey": { + "message": "Odstrániť prístupový kľúč" + }, + "passkeyRemoved": { + "message": "Prístupový kľúč bol odstránený" + }, + "errorAssigningTargetCollection": { + "message": "Chyba pri priraďovaní cieľovej kolekcie." + }, + "errorAssigningTargetFolder": { + "message": "Chyba pri priraďovaní cieľového priečinka." } } diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index fac763dec9..8c9c158c87 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Spremeni glavno geslo" }, - "changeMasterPasswordConfirmation": { - "message": "Svoje glavno geslo lahko spremenite v bitwarden.com spletnem trezorju. Želite obiskati spletno stran zdaj?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Identifikacijsko geslo", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 73469b26b4..edafdb55a2 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Промени главну лозинку" }, - "changeMasterPasswordConfirmation": { - "message": "Можете променити главну лозинку у Вашем сефу на bitwarden.com. Да ли желите да посетите веб страницу сада?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Сигурносна Фраза Сефа", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Интеграција са претраживачем није подржана" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Нажалост, интеграција прегледача за сада је подржана само у верзији Mac App Store." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Омогућите хардверско убрзање и поново покрените" + }, + "removePasskey": { + "message": "Уклонити приступачни кључ" + }, + "passkeyRemoved": { + "message": "Приступачни кључ је уклоњен" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index c4c70123ac..6342424fd4 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Ändra huvudlösenord" }, - "changeMasterPasswordConfirmation": { - "message": "Du kan ändra ditt huvudlösenord i Bitwardens webbvalv. Vill du besöka webbplatsen nu?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingeravtrycksfras", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Webbläsarintegration stöds inte" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Tyvärr stöds webbläsarintegration för tillfället endast i versionen från Mac App Store." }, @@ -2689,12 +2698,24 @@ "description": "Label indicating the most common import formats" }, "troubleshooting": { - "message": "Troubleshooting" + "message": "Felsökning" }, "disableHardwareAccelerationRestart": { "message": "Disable hardware acceleration and restart" }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Ta bort nyckel" + }, + "passkeyRemoved": { + "message": "Nyckel borttagen" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 11694b8c9c..90648699c0 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index a664696d5b..5b6a1b9d0b 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "เปลี่ยนรหัสผ่านหลัก" }, - "changeMasterPasswordConfirmation": { - "message": "คุณสามารถเปลี่ยนรหัสผ่านหลักของคุณได้ที่เว็บ bitwarden.com คุณต้องการไปที่เว็บไซต์ตอนนี้ไหม?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint Phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index ae3f217aa6..42a8f207c7 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Ana parolayı değiştir" }, - "changeMasterPasswordConfirmation": { - "message": "Ana parolanızı bitwarden.com web kasası üzerinden değiştirebilirsiniz. Siteyi şimdi ziyaret etmek ister misiniz?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Parmak izi ifadesi", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Tarayıcı entegrasyonu desteklenmiyor" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Ne yazık ki tarayıcı entegrasyonu şu anda sadece Mac App Store sürümünde destekleniyor." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Donanım hızlandırmayı etkinleştirin ve yeniden başlatın" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index cc5273b1bd..ac7b7c3243 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Змінити головний пароль" }, - "changeMasterPasswordConfirmation": { - "message": "Ви можете змінити головний пароль в сховищі на bitwarden.com. Хочете перейти на вебсайт зараз?" + "continueToWebApp": { + "message": "Продовжити у вебпрограмі?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Ви можете змінити головний пароль у вебпрограмі Bitwarden." }, "fingerprintPhrase": { "message": "Фраза відбитка", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Інтеграція з браузером не підтримується" }, + "browserIntegrationErrorTitle": { + "message": "Помилка увімкнення інтеграції з браузером" + }, + "browserIntegrationErrorDesc": { + "message": "Під час увімкнення інтеграції з браузером сталася помилка." + }, "browserIntegrationMasOnlyDesc": { "message": "На жаль, зараз інтеграція з браузером підтримується лише у версії для Mac з App Store." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Увімкнути апаратне прискорення і перезапустити" + }, + "removePasskey": { + "message": "Вилучити ключ доступу" + }, + "passkeyRemoved": { + "message": "Ключ доступу вилучено" + }, + "errorAssigningTargetCollection": { + "message": "Помилка призначення цільової збірки." + }, + "errorAssigningTargetFolder": { + "message": "Помилка призначення цільової теки." } } diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index f81cb2778d..aac9995db1 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Thay đổi mật khẩu chính" }, - "changeMasterPasswordConfirmation": { - "message": "Bạn có thể thay đổi mật khẩu chính trong kho bitwarden nền web. Bạn có muốn truy cập trang web bây giờ?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint Phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Không hỗ trợ tích hợp trình duyệt" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Rất tiếc, tính năng tích hợp trình duyệt hiện chỉ được hỗ trợ trong phiên bản App Store trên Mac." }, @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 617e8dd934..0560466cf8 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "修改主密码" }, - "changeMasterPasswordConfirmation": { - "message": "您可以在 bitwarden.com 网页密码库修改您的主密码。现在要访问吗?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "指纹短语", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "不支持浏览器集成" }, + "browserIntegrationErrorTitle": { + "message": "启用浏览器集成时出错" + }, + "browserIntegrationErrorDesc": { + "message": "启用浏览器集成时出错。" + }, "browserIntegrationMasOnlyDesc": { "message": "很遗憾,目前仅 Mac App Store 版本支持浏览器集成。" }, @@ -1907,7 +1916,7 @@ "message": "无法完成生物识别。" }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "使用其他方式?" }, "useMasterPassword": { "message": "使用主密码" @@ -2696,5 +2705,17 @@ }, "enableHardwareAccelerationRestart": { "message": "启用硬件加速并重启" + }, + "removePasskey": { + "message": "移除通行密钥" + }, + "passkeyRemoved": { + "message": "通行密钥已移除" + }, + "errorAssigningTargetCollection": { + "message": "分配目标集合时出错。" + }, + "errorAssigningTargetFolder": { + "message": "分配目标文件夹时出错。" } } diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 1659344550..9eb12e23cf 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -404,7 +404,7 @@ "message": "長度" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "最小密碼長度" }, "uppercase": { "message": "大寫 (A-Z)" @@ -561,10 +561,10 @@ "message": "帳戶已建立!現在可以登入了。" }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "你已成功登入" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "你可以關閉此視窗" }, "masterPassSent": { "message": "已寄出包含您主密碼提示的電子郵件。" @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "變更主密碼" }, - "changeMasterPasswordConfirmation": { - "message": "您可以在 bitwarden.com 網頁版密碼庫變更主密碼。現在要前往嗎?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "指紋短語", @@ -1546,15 +1549,15 @@ "message": "設定主密碼" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "你的組織權限已更新,要求你設定一個主密碼。", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "你的組織要求你設定一個主密碼。", "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { - "message": "Verification required", + "message": "需要驗證", "description": "Default title for the user verification dialog." }, "currentMasterPass": { @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "不支援瀏覽器整合" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "很遺憾,目前僅 Mac App Store 版本支援瀏覽器整合功能。" }, @@ -1645,10 +1654,10 @@ "message": "在您的桌面和瀏覽器閒建立連綫時,透過要求指紋短語確認,以添加一個額外的安全層。每次建立連綫都需要使用者干預和驗證。" }, "enableHardwareAcceleration": { - "message": "Use hardware acceleration" + "message": "使用硬體加速" }, "enableHardwareAccelerationDesc": { - "message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required." + "message": "此設定預設為開啟。僅當你遇到圖形問題時才關閉。需要重新啟動。" }, "approve": { "message": "核准" @@ -1889,40 +1898,40 @@ "message": "您的主密碼不符合一個或多個組織原則要求。您必須立即更新您的主密碼才能存取密碼庫。進行此動作將登出您目前的工作階段,需要您重新登入。其他裝置上的工作階段可能繼續長達一小時。" }, "tryAgain": { - "message": "Try again" + "message": "再試一次" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "此操作需要驗證。設定 PIN 碼以繼續。" }, "setPin": { - "message": "Set PIN" + "message": "設定 PIN 碼" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "使用生物辨識進行驗證" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "正在等待確認" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "無法完成生物辨識。" }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "需要不同的方法嗎?" }, "useMasterPassword": { - "message": "Use master password" + "message": "使用主密碼" }, "usePin": { - "message": "Use PIN" + "message": "使用 PIN 碼" }, "useBiometrics": { - "message": "Use biometrics" + "message": "使用生物辨識" }, "enterVerificationCodeSentToEmail": { "message": "Enter the verification code that was sent to your email." }, "resendCode": { - "message": "Resend code" + "message": "重新傳送驗證碼" }, "hours": { "message": "小時" @@ -2465,7 +2474,7 @@ "message": "全部清除" }, "plusNMore": { - "message": "+ $QUANTITY$ more", + "message": "+ $QUANTITY$ 個更多", "placeholders": { "quantity": { "content": "$1", @@ -2477,7 +2486,7 @@ "message": "子選單" }, "skipToContent": { - "message": "Skip to content" + "message": "跳至內容" }, "typePasskey": { "message": "密碼金鑰" @@ -2621,13 +2630,13 @@ "message": "使用者名稱或密碼不正確" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "密碼不正確" }, "incorrectCode": { - "message": "Incorrect code" + "message": "驗證碼不正確" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "PIN 碼不正確" }, "multifactorAuthenticationFailed": { "message": "多因素驗證失敗" @@ -2685,16 +2694,28 @@ "message": "將與您的 LastPass 帳戶關聯的 YubiKey 插入電腦的 USB 連接埠,然後觸摸其按鈕。" }, "commonImportFormats": { - "message": "Common formats", + "message": "常見格式", "description": "Label indicating the most common import formats" }, "troubleshooting": { - "message": "Troubleshooting" + "message": "疑難排解" }, "disableHardwareAccelerationRestart": { - "message": "Disable hardware acceleration and restart" + "message": "停用硬體加速並重新啟動" }, "enableHardwareAccelerationRestart": { - "message": "Enable hardware acceleration and restart" + "message": "啟用硬體加速並重新啟動" + }, + "removePasskey": { + "message": "移除金鑰" + }, + "passkeyRemoved": { + "message": "金鑰已移除" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 67f08839c5..0655e5600d 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -1,7 +1,7 @@ import * as path from "path"; import { app } from "electron"; -import { firstValueFrom } from "rxjs"; +import { Subject, firstValueFrom } from "rxjs"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; @@ -11,6 +11,9 @@ import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwar import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { DefaultBiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message, MessageSender } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- For dependency creation +import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; @@ -18,7 +21,6 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; -import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service"; /* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally this should not be accessed */ import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider"; @@ -59,7 +61,7 @@ export class Main { storageService: ElectronStorageService; memoryStorageService: MemoryStorageService; memoryStorageForStateProviders: MemoryStorageServiceForStateProviders; - messagingService: ElectronMainMessagingService; + messagingService: MessageSender; stateService: StateService; environmentService: DefaultEnvironmentService; mainCryptoFunctionService: MainCryptoFunctionService; @@ -131,7 +133,7 @@ export class Main { this.i18nService = new I18nMainService("en", "./locales/", globalStateProvider); const accountService = new AccountServiceImplementation( - new NoopMessagingService(), + MessageSender.EMPTY, this.logService, globalStateProvider, ); @@ -223,7 +225,13 @@ export class Main { this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain); this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.desktopSettingsService); - this.messagingService = new ElectronMainMessagingService(this.windowMain, (message) => { + const messageSubject = new Subject>(); + this.messagingService = MessageSender.combine( + new SubjectMessageSender(messageSubject), // For local messages + new ElectronMainMessagingService(this.windowMain), + ); + + messageSubject.asObservable().subscribe((message) => { this.messagingMain.onMessage(message); }); @@ -291,12 +299,20 @@ export class Main { this.powerMonitorMain.init(); await this.updaterMain.init(); - if ( - (await this.stateService.getEnableBrowserIntegration()) || - (await firstValueFrom( - this.desktopAutofillSettingsService.enableDuckDuckGoBrowserIntegration$, - )) - ) { + const [browserIntegrationEnabled, ddgIntegrationEnabled] = await Promise.all([ + this.stateService.getEnableBrowserIntegration(), + firstValueFrom(this.desktopAutofillSettingsService.enableDuckDuckGoBrowserIntegration$), + ]); + + if (browserIntegrationEnabled || ddgIntegrationEnabled) { + // Re-register the native messaging host integrations on startup, in case they are not present + if (browserIntegrationEnabled) { + this.nativeMessagingMain.generateManifests().catch(this.logService.error); + } + if (ddgIntegrationEnabled) { + this.nativeMessagingMain.generateDdgManifests().catch(this.logService.error); + } + this.nativeMessagingMain.listen(); } diff --git a/apps/desktop/src/main/menu/menu.account.ts b/apps/desktop/src/main/menu/menu.account.ts index 142431ae0d..f3c9b08531 100644 --- a/apps/desktop/src/main/menu/menu.account.ts +++ b/apps/desktop/src/main/menu/menu.account.ts @@ -65,10 +65,10 @@ export class AccountMenu implements IMenubarMenu { id: "changeMasterPass", click: async () => { const result = await dialog.showMessageBox(this._window, { - title: this.localize("changeMasterPass"), - message: this.localize("changeMasterPass"), - detail: this.localize("changeMasterPasswordConfirmation"), - buttons: [this.localize("yes"), this.localize("no")], + title: this.localize("continueToWebApp"), + message: this.localize("continueToWebApp"), + detail: this.localize("changeMasterPasswordOnWebConfirmation"), + buttons: [this.localize("continue"), this.localize("cancel")], cancelId: 1, defaultId: 0, noLink: true, diff --git a/apps/desktop/src/main/messaging.main.ts b/apps/desktop/src/main/messaging.main.ts index 256d551560..a9f80b7d20 100644 --- a/apps/desktop/src/main/messaging.main.ts +++ b/apps/desktop/src/main/messaging.main.ts @@ -75,22 +75,6 @@ export class MessagingMain { case "getWindowIsFocused": this.windowIsFocused(); break; - case "enableBrowserIntegration": - this.main.nativeMessagingMain.generateManifests(); - this.main.nativeMessagingMain.listen(); - break; - case "enableDuckDuckGoBrowserIntegration": - this.main.nativeMessagingMain.generateDdgManifests(); - this.main.nativeMessagingMain.listen(); - break; - case "disableBrowserIntegration": - this.main.nativeMessagingMain.removeManifests(); - this.main.nativeMessagingMain.stop(); - break; - case "disableDuckDuckGoBrowserIntegration": - this.main.nativeMessagingMain.removeDdgManifests(); - this.main.nativeMessagingMain.stop(); - break; default: break; } diff --git a/apps/desktop/src/main/native-messaging.main.ts b/apps/desktop/src/main/native-messaging.main.ts index 05e987e20b..d3dd25c644 100644 --- a/apps/desktop/src/main/native-messaging.main.ts +++ b/apps/desktop/src/main/native-messaging.main.ts @@ -22,7 +22,55 @@ export class NativeMessagingMain { private windowMain: WindowMain, private userPath: string, private exePath: string, - ) {} + ) { + ipcMain.handle( + "nativeMessaging.manifests", + async (_event: any, options: { create: boolean }) => { + if (options.create) { + this.listen(); + try { + await this.generateManifests(); + } catch (e) { + this.logService.error("Error generating manifests: " + e); + return e; + } + } else { + this.stop(); + try { + await this.removeManifests(); + } catch (e) { + this.logService.error("Error removing manifests: " + e); + return e; + } + } + return null; + }, + ); + + ipcMain.handle( + "nativeMessaging.ddgManifests", + async (_event: any, options: { create: boolean }) => { + if (options.create) { + this.listen(); + try { + await this.generateDdgManifests(); + } catch (e) { + this.logService.error("Error generating duckduckgo manifests: " + e); + return e; + } + } else { + this.stop(); + try { + await this.removeDdgManifests(); + } catch (e) { + this.logService.error("Error removing duckduckgo manifests: " + e); + return e; + } + } + return null; + }, + ); + } listen() { ipc.config.id = "bitwarden"; @@ -76,7 +124,7 @@ export class NativeMessagingMain { ipc.server.emit(socket, "message", message); } - generateManifests() { + async generateManifests() { const baseJson = { name: "com.8bit.bitwarden", description: "Bitwarden desktop <-> browser bridge", @@ -84,6 +132,10 @@ export class NativeMessagingMain { type: "stdio", }; + if (!existsSync(baseJson.path)) { + throw new Error(`Unable to find binary: ${baseJson.path}`); + } + const firefoxJson = { ...baseJson, ...{ allowed_extensions: ["{446900e4-71c2-419f-a6a7-df9c091e268b}"] }, @@ -92,8 +144,11 @@ export class NativeMessagingMain { ...baseJson, ...{ allowed_origins: [ + // Chrome extension "chrome-extension://nngceckbapebfimnlniiiahkandclblb/", + // Edge extension "chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh/", + // Opera extension "chrome-extension://ccnckbpmaceehanjmeomladnmlffdjgn/", ], }, @@ -102,27 +157,17 @@ export class NativeMessagingMain { switch (process.platform) { case "win32": { const destination = path.join(this.userPath, "browsers"); - // 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.writeManifest(path.join(destination, "firefox.json"), firefoxJson); - // 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.writeManifest(path.join(destination, "chrome.json"), chromeJson); + await this.writeManifest(path.join(destination, "firefox.json"), firefoxJson); + await this.writeManifest(path.join(destination, "chrome.json"), chromeJson); - // 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.createWindowsRegistry( - "HKLM\\SOFTWARE\\Mozilla\\Firefox", - "HKCU\\SOFTWARE\\Mozilla\\NativeMessagingHosts\\com.8bit.bitwarden", - path.join(destination, "firefox.json"), - ); - // 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.createWindowsRegistry( - "HKCU\\SOFTWARE\\Google\\Chrome", - "HKCU\\SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\com.8bit.bitwarden", - path.join(destination, "chrome.json"), - ); + const nmhs = this.getWindowsNMHS(); + for (const [key, value] of Object.entries(nmhs)) { + let manifestPath = path.join(destination, "chrome.json"); + if (key === "Firefox") { + manifestPath = path.join(destination, "firefox.json"); + } + await this.createWindowsRegistry(value, manifestPath); + } break; } case "darwin": { @@ -136,38 +181,30 @@ export class NativeMessagingMain { manifest = firefoxJson; } - this.writeManifest(p, manifest).catch((e) => - this.logService.error(`Error writing manifest for ${key}. ${e}`), - ); + await this.writeManifest(p, manifest); } else { - this.logService.warning(`${key} not found skipping.`); + this.logService.warning(`${key} not found, skipping.`); } } break; } case "linux": if (existsSync(`${this.homedir()}/.mozilla/`)) { - // 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.writeManifest( + await this.writeManifest( `${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`, firefoxJson, ); } if (existsSync(`${this.homedir()}/.config/google-chrome/`)) { - // 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.writeManifest( + await this.writeManifest( `${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`, chromeJson, ); } if (existsSync(`${this.homedir()}/.config/microsoft-edge/`)) { - // 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.writeManifest( + await this.writeManifest( `${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`, chromeJson, ); @@ -178,20 +215,23 @@ export class NativeMessagingMain { } } - generateDdgManifests() { + async generateDdgManifests() { const manifest = { name: "com.8bit.bitwarden", description: "Bitwarden desktop <-> DuckDuckGo bridge", path: this.binaryPath(), type: "stdio", }; + + if (!existsSync(manifest.path)) { + throw new Error(`Unable to find binary: ${manifest.path}`); + } + switch (process.platform) { case "darwin": { /* eslint-disable-next-line no-useless-escape */ const path = `${this.homedir()}/Library/Containers/com.duckduckgo.macos.browser/Data/Library/Application\ Support/NativeMessagingHosts/com.8bit.bitwarden.json`; - this.writeManifest(path, manifest).catch((e) => - this.logService.error(`Error writing manifest for DuckDuckGo. ${e}`), - ); + await this.writeManifest(path, manifest); break; } default: @@ -199,86 +239,50 @@ export class NativeMessagingMain { } } - removeManifests() { + async removeManifests() { switch (process.platform) { - case "win32": - // 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 - fs.unlink(path.join(this.userPath, "browsers", "firefox.json")); - // 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 - fs.unlink(path.join(this.userPath, "browsers", "chrome.json")); - // 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.deleteWindowsRegistry( - "HKCU\\SOFTWARE\\Mozilla\\NativeMessagingHosts\\com.8bit.bitwarden", - ); - // 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.deleteWindowsRegistry( - "HKCU\\SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\com.8bit.bitwarden", - ); + case "win32": { + await this.removeIfExists(path.join(this.userPath, "browsers", "firefox.json")); + await this.removeIfExists(path.join(this.userPath, "browsers", "chrome.json")); + + const nmhs = this.getWindowsNMHS(); + for (const [, value] of Object.entries(nmhs)) { + await this.deleteWindowsRegistry(value); + } break; + } case "darwin": { const nmhs = this.getDarwinNMHS(); for (const [, value] of Object.entries(nmhs)) { - const p = path.join(value, "NativeMessagingHosts", "com.8bit.bitwarden.json"); - if (existsSync(p)) { - // 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 - fs.unlink(p); - } + await this.removeIfExists( + path.join(value, "NativeMessagingHosts", "com.8bit.bitwarden.json"), + ); } break; } - case "linux": - if ( - existsSync(`${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`) - ) { - // 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 - fs.unlink(`${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`); - } - - if ( - existsSync( - `${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`, - ) - ) { - // 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 - fs.unlink( - `${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`, - ); - } - - if ( - existsSync( - `${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`, - ) - ) { - // 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 - fs.unlink( - `${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`, - ); - } + case "linux": { + await this.removeIfExists( + `${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`, + ); + await this.removeIfExists( + `${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`, + ); + await this.removeIfExists( + `${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`, + ); break; + } default: break; } } - removeDdgManifests() { + async removeDdgManifests() { switch (process.platform) { case "darwin": { /* eslint-disable-next-line no-useless-escape */ const path = `${this.homedir()}/Library/Containers/com.duckduckgo.macos.browser/Data/Library/Application\ Support/NativeMessagingHosts/com.8bit.bitwarden.json`; - if (existsSync(path)) { - // 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 - fs.unlink(path); - } + await this.removeIfExists(path); break; } default: @@ -286,6 +290,16 @@ export class NativeMessagingMain { } } + private getWindowsNMHS() { + return { + Firefox: "HKCU\\SOFTWARE\\Mozilla\\NativeMessagingHosts\\com.8bit.bitwarden", + Chrome: "HKCU\\SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\com.8bit.bitwarden", + Chromium: "HKCU\\SOFTWARE\\Chromium\\NativeMessagingHosts\\com.8bit.bitwarden", + // Edge uses the same registry key as Chrome as a fallback, but it's has its own separate key as well. + "Microsoft Edge": "HKCU\\SOFTWARE\\Microsoft\\Edge\\NativeMessagingHosts\\com.8bit.bitwarden", + }; + } + private getDarwinNMHS() { /* eslint-disable no-useless-escape */ return { @@ -305,10 +319,13 @@ export class NativeMessagingMain { } private async writeManifest(destination: string, manifest: object) { + this.logService.debug(`Writing manifest: ${destination}`); + if (!existsSync(path.dirname(destination))) { await fs.mkdir(path.dirname(destination)); } - fs.writeFile(destination, JSON.stringify(manifest, null, 2)).catch(this.logService.error); + + await fs.writeFile(destination, JSON.stringify(manifest, null, 2)); } private binaryPath() { @@ -327,39 +344,26 @@ export class NativeMessagingMain { return regedit; } - private async createWindowsRegistry(check: string, location: string, jsonFile: string) { + private async createWindowsRegistry(location: string, jsonFile: string) { const regedit = this.getRegeditInstance(); - const list = util.promisify(regedit.list); const createKey = util.promisify(regedit.createKey); const putValue = util.promisify(regedit.putValue); this.logService.debug(`Adding registry: ${location}`); - // Check installed - try { - await list(check); - } catch { - this.logService.warning(`Not finding registry ${check} skipping.`); - return; - } + await createKey(location); - try { - await createKey(location); + // Insert path to manifest + const obj: any = {}; + obj[location] = { + default: { + value: jsonFile, + type: "REG_DEFAULT", + }, + }; - // Insert path to manifest - const obj: any = {}; - obj[location] = { - default: { - value: jsonFile, - type: "REG_DEFAULT", - }, - }; - - return putValue(obj); - } catch (error) { - this.logService.error(error); - } + return putValue(obj); } private async deleteWindowsRegistry(key: string) { @@ -385,4 +389,10 @@ export class NativeMessagingMain { return homedir(); } } + + private async removeIfExists(path: string) { + if (existsSync(path)) { + await fs.unlink(path); + } + } } diff --git a/apps/desktop/src/main/power-monitor.main.ts b/apps/desktop/src/main/power-monitor.main.ts index 067a380ba0..8cad5c1d9e 100644 --- a/apps/desktop/src/main/power-monitor.main.ts +++ b/apps/desktop/src/main/power-monitor.main.ts @@ -1,6 +1,7 @@ import { powerMonitor } from "electron"; -import { ElectronMainMessagingService } from "../services/electron-main-messaging.service"; +import { MessageSender } from "@bitwarden/common/platform/messaging"; + import { isSnapStore } from "../utils"; // tslint:disable-next-line @@ -10,7 +11,7 @@ const IdleCheckInterval = 30 * 1000; // 30 seconds export class PowerMonitorMain { private idle = false; - constructor(private messagingService: ElectronMainMessagingService) {} + constructor(private messagingService: MessageSender) {} init() { // ref: https://github.com/electron/electron/issues/13767 diff --git a/apps/desktop/src/main/updater.main.ts b/apps/desktop/src/main/updater.main.ts index c9a2dc0e14..0e2efa66f9 100644 --- a/apps/desktop/src/main/updater.main.ts +++ b/apps/desktop/src/main/updater.main.ts @@ -27,8 +27,7 @@ export class UpdaterMain { process.platform === "win32" && !isWindowsStore() && !isWindowsPortable(); const macCanUpdate = process.platform === "darwin" && !isMacAppStore(); this.canUpdate = - process.env.ELECTRON_NO_UPDATER !== "1" && - (linuxCanUpdate || windowsCanUpdate || macCanUpdate); + !this.userDisabledUpdates() && (linuxCanUpdate || windowsCanUpdate || macCanUpdate); } async init() { @@ -144,4 +143,13 @@ export class UpdaterMain { autoUpdater.autoDownload = true; this.doingUpdateCheck = false; } + + private userDisabledUpdates(): boolean { + for (const arg of process.argv) { + if (arg != null && arg.toUpperCase().indexOf("--ELECTRON_NO_UPDATER=1") > -1) { + return true; + } + } + return process.env.ELECTRON_NO_UPDATER === "1"; + } } diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 167c32cc81..11b38bd273 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.3.2", + "version": "2024.4.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.3.2", + "version": "2024.4.2", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-native": "file:../desktop_native", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index cfc0b9b4e2..a65dab016c 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2024.3.2", + "version": "2024.4.2", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/desktop/src/platform/flags.ts b/apps/desktop/src/platform/flags.ts index 0481c8ce9b..dc0103e243 100644 --- a/apps/desktop/src/platform/flags.ts +++ b/apps/desktop/src/platform/flags.ts @@ -7,13 +7,11 @@ import { } from "@bitwarden/common/platform/misc/flags"; // required to avoid linting errors when there are no flags -/* eslint-disable-next-line @typescript-eslint/ban-types */ -export type Flags = { - showDDGSetting?: boolean; -} & SharedFlags; +// eslint-disable-next-line @typescript-eslint/ban-types +export type Flags = {} & SharedFlags; // required to avoid linting errors when there are no flags -/* eslint-disable-next-line @typescript-eslint/ban-types */ +// eslint-disable-next-line @typescript-eslint/ban-types export type DevFlags = {} & SharedDevFlags; export function flagEnabled(flag: keyof Flags): boolean { diff --git a/apps/desktop/src/platform/preload.ts b/apps/desktop/src/platform/preload.ts index 1f6bd200e0..771d25ef0a 100644 --- a/apps/desktop/src/platform/preload.ts +++ b/apps/desktop/src/platform/preload.ts @@ -74,6 +74,13 @@ const nativeMessaging = { onMessage: (callback: (message: LegacyMessageWrapper | Message) => void) => { ipcRenderer.on("nativeMessaging", (_event, message) => callback(message)); }, + + manifests: { + generate: (create: boolean): Promise => + ipcRenderer.invoke("nativeMessaging.manifests", { create }), + generateDuckDuckGo: (create: boolean): Promise => + ipcRenderer.invoke("nativeMessaging.ddgManifests", { create }), + }, }; const crypto = { @@ -117,12 +124,21 @@ export default { sendMessage: (message: { command: string } & any) => ipcRenderer.send("messagingService", message), - onMessage: (callback: (message: { command: string } & any) => void) => { - ipcRenderer.on("messagingService", (_event, message: any) => { - if (message.command) { - callback(message); - } - }); + onMessage: { + addListener: (callback: (message: { command: string } & any) => void) => { + ipcRenderer.addListener("messagingService", (_event, message: any) => { + if (message.command) { + callback(message); + } + }); + }, + removeListener: (callback: (message: { command: string } & any) => void) => { + ipcRenderer.removeListener("messagingService", (_event, message: any) => { + if (message.command) { + callback(message); + } + }); + }, }, launchUri: (uri: string) => ipcRenderer.invoke("launchUri", uri), diff --git a/apps/desktop/src/platform/services/electron-crypto.service.spec.ts b/apps/desktop/src/platform/services/electron-crypto.service.spec.ts index 04adfcac70..3d9171b52e 100644 --- a/apps/desktop/src/platform/services/electron-crypto.service.spec.ts +++ b/apps/desktop/src/platform/services/electron-crypto.service.spec.ts @@ -1,6 +1,7 @@ import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider"; import { mock } from "jest-mock-extended"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; @@ -30,6 +31,7 @@ describe("electronCryptoService", () => { const platformUtilService = mock(); const logService = mock(); const stateService = mock(); + let masterPasswordService: FakeMasterPasswordService; let accountService: FakeAccountService; let stateProvider: FakeStateProvider; const biometricStateService = mock(); @@ -38,9 +40,11 @@ describe("electronCryptoService", () => { beforeEach(() => { accountService = mockAccountServiceWith("userId" as UserId); + masterPasswordService = new FakeMasterPasswordService(); stateProvider = new FakeStateProvider(accountService); sut = new ElectronCryptoService( + masterPasswordService, keyGenerationService, cryptoFunctionService, encryptService, diff --git a/apps/desktop/src/platform/services/electron-crypto.service.ts b/apps/desktop/src/platform/services/electron-crypto.service.ts index 6b9327a9c4..d113a18200 100644 --- a/apps/desktop/src/platform/services/electron-crypto.service.ts +++ b/apps/desktop/src/platform/services/electron-crypto.service.ts @@ -1,6 +1,7 @@ import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; @@ -20,6 +21,7 @@ import { UserKey, MasterKey } from "@bitwarden/common/types/key"; export class ElectronCryptoService extends CryptoService { constructor( + masterPasswordService: InternalMasterPasswordServiceAbstraction, keyGenerationService: KeyGenerationService, cryptoFunctionService: CryptoFunctionService, encryptService: EncryptService, @@ -31,6 +33,7 @@ export class ElectronCryptoService extends CryptoService { private biometricStateService: BiometricStateService, ) { super( + masterPasswordService, keyGenerationService, cryptoFunctionService, encryptService, @@ -159,12 +162,16 @@ export class ElectronCryptoService extends CryptoService { const oldBiometricKey = await this.stateService.getCryptoMasterKeyBiometric({ userId }); // decrypt const masterKey = new SymmetricCryptoKey(Utils.fromB64ToArray(oldBiometricKey)) as MasterKey; - let encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey(); - encUserKey = encUserKey ?? (await this.stateService.getMasterKeyEncryptedUserKey()); + userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id; + const encUserKeyPrim = await this.stateService.getEncryptedCryptoSymmetricKey(); + const encUserKey = + encUserKeyPrim != null + ? new EncString(encUserKeyPrim) + : await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId); if (!encUserKey) { throw new Error("No user key found during biometric migration"); } - const userKey = await this.decryptUserKeyWithMasterKey(masterKey, new EncString(encUserKey)); + const userKey = await this.decryptUserKeyWithMasterKey(masterKey, encUserKey); // migrate await this.storeBiometricKey(userKey, userId); await this.stateService.setCryptoMasterKeyBiometric(null, { userId }); diff --git a/apps/desktop/src/platform/services/electron-renderer-message.sender.ts b/apps/desktop/src/platform/services/electron-renderer-message.sender.ts new file mode 100644 index 0000000000..037c303b3b --- /dev/null +++ b/apps/desktop/src/platform/services/electron-renderer-message.sender.ts @@ -0,0 +1,12 @@ +import { MessageSender, CommandDefinition } from "@bitwarden/common/platform/messaging"; +import { getCommand } from "@bitwarden/common/platform/messaging/internal"; + +export class ElectronRendererMessageSender implements MessageSender { + send( + commandDefinition: CommandDefinition | string, + payload: object | T = {}, + ): void { + const command = getCommand(commandDefinition); + ipc.platform.sendMessage(Object.assign({}, { command: command }, payload)); + } +} diff --git a/apps/desktop/src/platform/services/electron-renderer-messaging.service.ts b/apps/desktop/src/platform/services/electron-renderer-messaging.service.ts deleted file mode 100644 index 192efc1dc6..0000000000 --- a/apps/desktop/src/platform/services/electron-renderer-messaging.service.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -export class ElectronRendererMessagingService implements MessagingService { - constructor(private broadcasterService: BroadcasterService) { - ipc.platform.onMessage((message) => this.sendMessage(message.command, message, false)); - } - - send(subscriber: string, arg: any = {}) { - this.sendMessage(subscriber, arg, true); - } - - private sendMessage(subscriber: string, arg: any = {}, toMain: boolean) { - const message = Object.assign({}, { command: subscriber }, arg); - this.broadcasterService.send(message); - if (toMain) { - ipc.platform.sendMessage(message); - } - } -} diff --git a/apps/desktop/src/platform/utils/from-ipc-messaging.ts b/apps/desktop/src/platform/utils/from-ipc-messaging.ts new file mode 100644 index 0000000000..254a215ceb --- /dev/null +++ b/apps/desktop/src/platform/utils/from-ipc-messaging.ts @@ -0,0 +1,15 @@ +import { fromEventPattern, share } from "rxjs"; + +import { Message } from "@bitwarden/common/platform/messaging"; +import { tagAsExternal } from "@bitwarden/common/platform/messaging/internal"; + +/** + * Creates an observable that when subscribed to will listen to messaging events through IPC. + * @returns An observable stream of messages. + */ +export const fromIpcMessaging = () => { + return fromEventPattern>( + (handler) => ipc.platform.onMessage.addListener(handler), + (handler) => ipc.platform.onMessage.removeListener(handler), + ).pipe(tagAsExternal, share()); +}; diff --git a/apps/desktop/src/scss/plugins.scss b/apps/desktop/src/scss/plugins.scss deleted file mode 100644 index c156456809..0000000000 --- a/apps/desktop/src/scss/plugins.scss +++ /dev/null @@ -1,95 +0,0 @@ -@import "~ngx-toastr/toastr"; - -@import "variables.scss"; - -.toast-container { - .toast-close-button { - @include themify($themes) { - color: themed("toastTextColor"); - } - font-size: 18px; - margin-right: 4px; - } - - .ngx-toastr { - @include themify($themes) { - color: themed("toastTextColor"); - } - align-items: center; - background-image: none !important; - border-radius: $border-radius; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.35); - display: flex; - padding: 15px; - - .toast-close-button { - position: absolute; - right: 5px; - top: 0; - } - - &:hover { - box-shadow: 0 0 10px rgba(0, 0, 0, 0.6); - } - - .icon i::before { - float: left; - font-style: normal; - font-family: $icomoon-font-family; - font-size: 25px; - line-height: 20px; - padding-right: 15px; - } - - .toast-message { - p { - margin-bottom: 0.5rem; - - &:last-child { - margin-bottom: 0; - } - } - } - - &.toast-danger, - &.toast-error { - @include themify($themes) { - background-color: themed("dangerColor"); - } - - .icon i::before { - content: map_get($icons, "error"); - } - } - - &.toast-warning { - @include themify($themes) { - background-color: themed("warningColor"); - } - - .icon i::before { - content: map_get($icons, "exclamation-triangle"); - } - } - - &.toast-info { - @include themify($themes) { - background-color: themed("infoColor"); - } - - .icon i:before { - content: map_get($icons, "info-circle"); - } - } - - &.toast-success { - @include themify($themes) { - background-color: themed("successColor"); - } - - .icon i:before { - content: map_get($icons, "check"); - } - } - } -} diff --git a/apps/desktop/src/scss/styles.scss b/apps/desktop/src/scss/styles.scss index 033a0f8b67..54c1385dcf 100644 --- a/apps/desktop/src/scss/styles.scss +++ b/apps/desktop/src/scss/styles.scss @@ -11,7 +11,6 @@ @import "buttons.scss"; @import "misc.scss"; @import "modal.scss"; -@import "plugins.scss"; @import "environment.scss"; @import "header.scss"; @import "left-nav.scss"; diff --git a/apps/desktop/src/services/electron-main-messaging.service.ts b/apps/desktop/src/services/electron-main-messaging.service.ts index 71e1b1d7d5..ce4ffd903a 100644 --- a/apps/desktop/src/services/electron-main-messaging.service.ts +++ b/apps/desktop/src/services/electron-main-messaging.service.ts @@ -2,18 +2,17 @@ import * as path from "path"; import { app, dialog, ipcMain, Menu, MenuItem, nativeTheme, Notification, shell } from "electron"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; +import { MessageSender, CommandDefinition } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- Using implementation helper in implementation +import { getCommand } from "@bitwarden/common/platform/messaging/internal"; import { SafeUrls } from "@bitwarden/common/platform/misc/safe-urls"; import { WindowMain } from "../main/window.main"; import { RendererMenuItem } from "../utils"; -export class ElectronMainMessagingService implements MessagingService { - constructor( - private windowMain: WindowMain, - private onMessage: (message: any) => void, - ) { +export class ElectronMainMessagingService implements MessageSender { + constructor(private windowMain: WindowMain) { ipcMain.handle("appVersion", () => { return app.getVersion(); }); @@ -88,9 +87,9 @@ export class ElectronMainMessagingService implements MessagingService { }); } - send(subscriber: string, arg: any = {}) { - const message = Object.assign({}, { command: subscriber }, arg); - this.onMessage(message); + send(commandDefinition: CommandDefinition | string, arg: T | object = {}) { + const command = getCommand(commandDefinition); + const message = Object.assign({}, { command: command }, arg); if (this.windowMain.win != null) { this.windowMain.win.webContents.send("messagingService", message); } diff --git a/apps/desktop/src/services/native-messaging.service.ts b/apps/desktop/src/services/native-messaging.service.ts index 148e4f1e89..01d9476977 100644 --- a/apps/desktop/src/services/native-messaging.service.ts +++ b/apps/desktop/src/services/native-messaging.service.ts @@ -1,6 +1,7 @@ import { Injectable, NgZone } from "@angular/core"; import { firstValueFrom } from "rxjs"; +import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -30,6 +31,7 @@ export class NativeMessagingService { private sharedSecrets = new Map(); constructor( + private masterPasswordService: MasterPasswordServiceAbstraction, private cryptoFunctionService: CryptoFunctionService, private cryptoService: CryptoService, private platformUtilService: PlatformUtilsService, @@ -162,7 +164,9 @@ export class NativeMessagingService { KeySuffixOptions.Biometric, message.userId, ); - const masterKey = await this.cryptoService.getMasterKey(message.userId); + const masterKey = await firstValueFrom( + this.masterPasswordService.masterKey$(message.userId as UserId), + ); if (userKey != null) { // we send the master key still for backwards compatibility 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 b89beebaa6..86e0b881ee 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.ts +++ b/apps/desktop/src/vault/app/vault/add-edit.component.ts @@ -75,6 +75,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnChanges, async ngOnInit() { await super.ngOnInit(); + await this.load(); this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { this.ngZone.run(() => { switch (message.command) { diff --git a/apps/desktop/src/vault/app/vault/vault.component.ts b/apps/desktop/src/vault/app/vault/vault.component.ts index e8aabbb20f..208bbc70f0 100644 --- a/apps/desktop/src/vault/app/vault/vault.component.ts +++ b/apps/desktop/src/vault/app/vault/vault.component.ts @@ -8,7 +8,7 @@ import { ViewContainerRef, } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; +import { firstValueFrom, Subject, takeUntil } from "rxjs"; import { first } from "rxjs/operators"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; @@ -16,13 +16,13 @@ import { ModalService } from "@bitwarden/angular/services/modal.service"; import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -32,6 +32,7 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { DialogService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; +import { AuthRequestServiceAbstraction } from "../../../../../../libs/auth/src/common/abstractions"; import { SearchBarService } from "../../../app/layout/search/search-bar.service"; import { GeneratorComponent } from "../../../app/tools/generator.component"; import { invokeMenu, RendererMenuItem } from "../../../utils"; @@ -102,11 +103,12 @@ export class VaultComponent implements OnInit, OnDestroy { private eventCollectionService: EventCollectionService, private totpService: TotpService, private passwordRepromptService: PasswordRepromptService, - private stateService: StateService, private searchBarService: SearchBarService, private apiService: ApiService, private dialogService: DialogService, private billingAccountProfileStateService: BillingAccountProfileStateService, + private authRequestService: AuthRequestServiceAbstraction, + private accountService: AccountService, ) {} async ngOnInit() { @@ -224,7 +226,8 @@ export class VaultComponent implements OnInit, OnDestroy { this.searchBarService.setEnabled(true); this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault")); - const approveLoginRequests = await this.stateService.getApproveLoginRequests(); + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + const approveLoginRequests = await this.authRequestService.getAcceptAuthRequests(userId); if (approveLoginRequests) { const authRequest = await this.apiService.getLastAuthRequest(); if (authRequest != null) { diff --git a/apps/web/config/base.json b/apps/web/config/base.json index a377298c63..5dc03a4633 100644 --- a/apps/web/config/base.json +++ b/apps/web/config/base.json @@ -11,7 +11,6 @@ "allowedHosts": "auto" }, "flags": { - "secretsManager": false, "showPasswordless": false, "enableCipherKeyEncryption": false } diff --git a/apps/web/config/cloud.json b/apps/web/config/cloud.json index 6e5c65af1d..3faa292692 100644 --- a/apps/web/config/cloud.json +++ b/apps/web/config/cloud.json @@ -17,7 +17,6 @@ "proxyNotifications": "https://notifications.bitwarden.com" }, "flags": { - "secretsManager": true, "showPasswordless": true, "enableCipherKeyEncryption": false } diff --git a/apps/web/config/development.json b/apps/web/config/development.json index 97742aee3d..44391a7450 100644 --- a/apps/web/config/development.json +++ b/apps/web/config/development.json @@ -20,7 +20,6 @@ } ], "flags": { - "secretsManager": true, "showPasswordless": true, "enableCipherKeyEncryption": false }, diff --git a/apps/web/config/euprd.json b/apps/web/config/euprd.json index 4b6c9fa909..72f0c1857d 100644 --- a/apps/web/config/euprd.json +++ b/apps/web/config/euprd.json @@ -11,7 +11,6 @@ "buttonAction": "https://www.paypal.com/cgi-bin/webscr" }, "flags": { - "secretsManager": true, "showPasswordless": true, "enableCipherKeyEncryption": false } diff --git a/apps/web/config/euqa.json b/apps/web/config/euqa.json index 70caf3db62..5f74eb8829 100644 --- a/apps/web/config/euqa.json +++ b/apps/web/config/euqa.json @@ -21,7 +21,6 @@ } ], "flags": { - "secretsManager": true, "showPasswordless": true } } diff --git a/apps/web/config/qa.json b/apps/web/config/qa.json index 0ce5f3dc7f..ac36b10784 100644 --- a/apps/web/config/qa.json +++ b/apps/web/config/qa.json @@ -27,7 +27,6 @@ } ], "flags": { - "secretsManager": true, "showPasswordless": true, "enableCipherKeyEncryption": false } diff --git a/apps/web/config/selfhosted.json b/apps/web/config/selfhosted.json index e16df20ad5..7e916a1116 100644 --- a/apps/web/config/selfhosted.json +++ b/apps/web/config/selfhosted.json @@ -7,7 +7,6 @@ "port": 8081 }, "flags": { - "secretsManager": true, "showPasswordless": true, "enableCipherKeyEncryption": false } diff --git a/apps/web/config/usdev.json b/apps/web/config/usdev.json index 9b794d896d..af96a38c6a 100644 --- a/apps/web/config/usdev.json +++ b/apps/web/config/usdev.json @@ -5,7 +5,6 @@ "scim": "https://scim.usdev.bitwarden.pw" }, "flags": { - "secretsManager": true, "showPasswordless": true } } diff --git a/apps/web/package.json b/apps/web/package.json index 5b049dcb9d..55fe0987d7 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.3.1", + "version": "2024.4.1", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", @@ -8,7 +8,7 @@ "build:bit:watch": "webpack serve -c ../../bitwarden_license/bit-web/webpack.config.js", "build:bit:dev": "cross-env ENV=development npm run build:bit", "build:bit:dev:analyze": "cross-env LOGGING=false webpack -c ../../bitwarden_license/bit-web/webpack.config.js --profile --json > stats.json && npx webpack-bundle-analyzer stats.json build/", - "build:bit:dev:watch": "cross-env ENV=development npm run build:bit:watch", + "build:bit:dev:watch": "cross-env ENV=development NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:bit:watch", "build:bit:qa": "cross-env NODE_ENV=production ENV=qa npm run build:bit", "build:bit:euprd": "cross-env NODE_ENV=production ENV=euprd npm run build:bit", "build:bit:euqa": "cross-env NODE_ENV=production ENV=euqa npm run build:bit", diff --git a/apps/web/src/app/admin-console/common/base.people.component.ts b/apps/web/src/app/admin-console/common/base.people.component.ts index 0a1f4338ff..fbb9faf569 100644 --- a/apps/web/src/app/admin-console/common/base.people.component.ts +++ b/apps/web/src/app/admin-console/common/base.people.component.ts @@ -1,5 +1,5 @@ -import { Directive, ViewChild, ViewContainerRef } from "@angular/core"; -import { firstValueFrom } from "rxjs"; +import { Directive, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; +import { BehaviorSubject, Subject, firstValueFrom, from, switchMap, takeUntil } from "rxjs"; import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; @@ -32,8 +32,10 @@ const MaxCheckedCount = 500; @Directive() export abstract class BasePeopleComponent< - UserType extends ProviderUserUserDetailsResponse | OrganizationUserView, -> { + UserType extends ProviderUserUserDetailsResponse | OrganizationUserView, + > + implements OnInit, OnDestroy +{ @ViewChild("confirmTemplate", { read: ViewContainerRef, static: true }) confirmModalRef: ViewContainerRef; @@ -88,7 +90,6 @@ export abstract class BasePeopleComponent< status: StatusType; users: UserType[] = []; pagedUsers: UserType[] = []; - searchText: string; actionPromise: Promise; protected allUsers: UserType[] = []; @@ -97,7 +98,19 @@ export abstract class BasePeopleComponent< protected didScroll = false; protected pageSize = 100; + protected destroy$ = new Subject(); + private pagedUsersCount = 0; + private _searchText$ = new BehaviorSubject(""); + private isSearching: boolean = false; + + get searchText() { + return this._searchText$.value; + } + + set searchText(value: string) { + this._searchText$.next(value); + } constructor( protected apiService: ApiService, @@ -122,6 +135,22 @@ export abstract class BasePeopleComponent< abstract reinviteUser(id: string): Promise; abstract confirmUser(user: UserType, publicKey: Uint8Array): Promise; + ngOnInit(): void { + this._searchText$ + .pipe( + switchMap((searchText) => from(this.searchService.isSearchable(searchText))), + takeUntil(this.destroy$), + ) + .subscribe((isSearchable) => { + this.isSearching = isSearchable; + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + async load() { const response = await this.getUsers(); this.statusMap.clear(); @@ -390,12 +419,8 @@ export abstract class BasePeopleComponent< } } - isSearching() { - return this.searchService.isSearchable(this.searchText); - } - isPaging() { - const searching = this.isSearching(); + const searching = this.isSearching; if (searching && this.didScroll) { // 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 diff --git a/apps/web/src/app/admin-console/icons/devices.ts b/apps/web/src/app/admin-console/icons/devices.ts index 348c836c4b..9faddb0b2e 100644 --- a/apps/web/src/app/admin-console/icons/devices.ts +++ b/apps/web/src/app/admin-console/icons/devices.ts @@ -3,15 +3,15 @@ import { svgIcon } from "@bitwarden/components"; export const Devices = svgIcon` - - - - - - - - - + + + + + + + + + `; diff --git a/apps/web/src/app/admin-console/organizations/create/organization-information.component.html b/apps/web/src/app/admin-console/organizations/create/organization-information.component.html index 6029cfd833..e0a8006081 100644 --- a/apps/web/src/app/admin-console/organizations/create/organization-information.component.html +++ b/apps/web/src/app/admin-console/organizations/create/organization-information.component.html @@ -20,19 +20,4 @@
-
- - {{ "accountOwnedBusiness" | i18n }} -
- - {{ "businessName" | i18n }} - - -
-
diff --git a/apps/web/src/app/admin-console/organizations/create/organization-information.component.ts b/apps/web/src/app/admin-console/organizations/create/organization-information.component.ts index 99cb3102aa..602ad82972 100644 --- a/apps/web/src/app/admin-console/organizations/create/organization-information.component.ts +++ b/apps/web/src/app/admin-console/organizations/create/organization-information.component.ts @@ -1,15 +1,32 @@ -import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { UntypedFormGroup } from "@angular/forms"; +import { firstValueFrom } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @Component({ selector: "app-org-info", templateUrl: "organization-information.component.html", }) -export class OrganizationInformationComponent { +export class OrganizationInformationComponent implements OnInit { @Input() nameOnly = false; @Input() createOrganization = true; @Input() isProvider = false; @Input() acceptingSponsorship = false; @Input() formGroup: UntypedFormGroup; @Output() changedBusinessOwned = new EventEmitter(); + + constructor(private accountService: AccountService) {} + + async ngOnInit(): Promise { + if (this.formGroup.controls.billingEmail.value) { + return; + } + + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + + if (activeAccount?.email) { + this.formGroup.controls.billingEmail.setValue(activeAccount.email); + } + } } diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.ts b/apps/web/src/app/admin-console/organizations/manage/events.component.ts index 20a0ef6e42..9fb9015155 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.ts @@ -154,7 +154,7 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe if (r.serviceAccountId) { return { - name: this.i18nService.t("serviceAccount") + " " + this.getShortId(r.serviceAccountId), + name: this.i18nService.t("machineAccount") + " " + this.getShortId(r.serviceAccountId), }; } diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html index c6bfb94557..3afb816e14 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html @@ -31,7 +31,12 @@ -

{{ "editGroupMembersDesc" | i18n }}

+

+ {{ "editGroupMembersDesc" | i18n }} + + {{ "restrictedGroupAccessDesc" | i18n }} + +

= []; group: GroupView; groupForm = this.formBuilder.group({ @@ -110,6 +125,10 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { return this.params.organizationId; } + protected get editMode(): boolean { + return this.groupId != null; + } + private destroy$ = new Subject(); private get orgCollections$() { @@ -134,7 +153,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { ); } - private get orgMembers$() { + private get orgMembers$(): Observable> { return from(this.organizationUserService.getAllUsers(this.organizationId)).pipe( map((response) => response.data.map((m) => ({ @@ -145,34 +164,55 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { listName: m.name?.length > 0 ? `${m.name} (${m.email})` : m.email, labelName: m.name || m.email, status: m.status, + userId: m.userId as UserId, })), ), ); } - private get groupDetails$() { - if (!this.editMode) { - return of(undefined); - } - - return combineLatest([ - this.groupService.get(this.organizationId, this.groupId), - this.apiService.getGroupUsers(this.organizationId, this.groupId), - ]).pipe( - map(([groupView, users]) => { - groupView.members = users; - return groupView; - }), - catchError((e: unknown) => { - if (e instanceof ErrorResponse) { - this.logService.error(e.message); - } else { - this.logService.error(e.toString()); - } + private groupDetails$: Observable = of(this.editMode).pipe( + concatMap((editMode) => { + if (!editMode) { return of(undefined); - }), - ); - } + } + + return combineLatest([ + this.groupService.get(this.organizationId, this.groupId), + this.apiService.getGroupUsers(this.organizationId, this.groupId), + ]).pipe( + map(([groupView, users]) => { + groupView.members = users; + return groupView; + }), + catchError((e: unknown) => { + if (e instanceof ErrorResponse) { + this.logService.error(e.message); + } else { + this.logService.error(e.toString()); + } + return of(undefined); + }), + ); + }), + shareReplay({ refCount: false }), + ); + + restrictGroupAccess$ = combineLatest([ + this.organizationService.get$(this.organizationId), + this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1), + this.groupDetails$, + ]).pipe( + map( + ([organization, flexibleCollectionsV1Enabled, group]) => + // Feature flag conditionals + flexibleCollectionsV1Enabled && + organization.flexibleCollections && + // Business logic conditionals + !organization.allowAdminAccessToAllCollectionItems && + group !== undefined, + ), + shareReplay({ refCount: true, bufferSize: 1 }), + ); constructor( @Inject(DIALOG_DATA) private params: GroupAddEditDialogParams, @@ -188,17 +228,25 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { private changeDetectorRef: ChangeDetectorRef, private dialogService: DialogService, private organizationService: OrganizationService, + private configService: ConfigService, + private accountService: AccountService, ) { this.tabIndex = params.initialTab ?? GroupAddEditTabType.Info; } ngOnInit() { - this.editMode = this.loading = this.groupId != null; + this.loading = true; this.title = this.i18nService.t(this.editMode ? "editGroup" : "newGroup"); - combineLatest([this.orgCollections$, this.orgMembers$, this.groupDetails$]) + combineLatest([ + this.orgCollections$, + this.orgMembers$, + this.groupDetails$, + this.restrictGroupAccess$, + this.accountService.activeAccount$, + ]) .pipe(takeUntil(this.destroy$)) - .subscribe(([collections, members, group]) => { + .subscribe(([collections, members, group, restrictGroupAccess, activeAccount]) => { this.collections = collections; this.members = members; this.group = group; @@ -224,6 +272,18 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { }); } + // If the current user is not already in the group and cannot add themselves, remove them from the list + if (restrictGroupAccess) { + const organizationUserId = this.members.find((m) => m.userId === activeAccount.id).id; + const isAlreadyInGroup = this.groupForm.value.members.some( + (m) => m.id === organizationUserId, + ); + + if (!isAlreadyInGroup) { + this.members = this.members.filter((m) => m.id !== organizationUserId); + } + } + this.loading = false; }); } 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 a41d57f874..9ff596181e 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 @@ -91,15 +91,16 @@ export class GroupsComponent implements OnInit, OnDestroy { private pagedGroupsCount = 0; private pagedGroups: GroupDetailsRow[]; private searchedGroups: GroupDetailsRow[]; - private _searchText: string; + private _searchText$ = new BehaviorSubject(""); private destroy$ = new Subject(); private refreshGroups$ = new BehaviorSubject(null); + private isSearching: boolean = false; get searchText() { - return this._searchText; + return this._searchText$.value; } set searchText(value: string) { - this._searchText = value; + this._searchText$.next(value); // Manually update as we are not using the search pipe in the template this.updateSearchedGroups(); } @@ -114,7 +115,7 @@ export class GroupsComponent implements OnInit, OnDestroy { if (this.isPaging()) { return this.pagedGroups; } - if (this.isSearching()) { + if (this.isSearching) { return this.searchedGroups; } return this.groups; @@ -180,6 +181,15 @@ export class GroupsComponent implements OnInit, OnDestroy { takeUntil(this.destroy$), ) .subscribe(); + + this._searchText$ + .pipe( + switchMap((searchText) => this.searchService.isSearchable(searchText)), + takeUntil(this.destroy$), + ) + .subscribe((isSearchable) => { + this.isSearching = isSearchable; + }); } ngOnDestroy() { @@ -297,10 +307,6 @@ export class GroupsComponent implements OnInit, OnDestroy { this.loadMore(); } - isSearching() { - return this.searchService.isSearchable(this.searchText); - } - check(groupRow: GroupDetailsRow) { groupRow.checked = !groupRow.checked; } @@ -310,7 +316,7 @@ export class GroupsComponent implements OnInit, OnDestroy { } isPaging() { - const searching = this.isSearching(); + const searching = this.isSearching; if (searching && this.didScroll) { this.resetPaging(); } @@ -340,7 +346,7 @@ export class GroupsComponent implements OnInit, OnDestroy { } private updateSearchedGroups() { - if (this.searchService.isSearchable(this.searchText)) { + if (this.isSearching) { // Making use of the pipe in the component as we need know which groups where filtered this.searchedGroups = this.searchPipe.transform( this.groups, 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 95febbd3c5..4d81d070fb 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 @@ -385,7 +385,12 @@

{{ "secretsManagerAccessDescription" | i18n }}

- + {{ "userAccessSecretsManagerGA" | i18n }} diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index 752122de00..f1af950650 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -31,6 +31,7 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v import { DialogService } from "@bitwarden/components"; import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service"; +import { CollectionAdminView } from "../../../../../vault/core/views/collection-admin.view"; import { CollectionAccessSelectionView, GroupService, @@ -62,6 +63,7 @@ export interface MemberDialogParams { organizationUserId: string; allOrganizationUserEmails: string[]; usesKeyConnector: boolean; + isOnSecretsManagerStandalone: boolean; initialTab?: MemberDialogTab; numConfirmedMembers: number; } @@ -87,6 +89,7 @@ export class MemberDialogComponent implements OnDestroy { organizationUserType = OrganizationUserType; PermissionMode = PermissionMode; showNoMasterPasswordWarning = false; + isOnSecretsManagerStandalone: boolean; protected organization$: Observable; protected collectionAccessItems: AccessItemView[] = []; @@ -159,6 +162,13 @@ export class MemberDialogComponent implements OnDestroy { this.editMode = this.params.organizationUserId != null; this.tabIndex = this.params.initialTab ?? MemberDialogTab.Role; this.title = this.i18nService.t(this.editMode ? "editMember" : "inviteMember"); + this.isOnSecretsManagerStandalone = this.params.isOnSecretsManagerStandalone; + + if (this.isOnSecretsManagerStandalone) { + this.formGroup.patchValue({ + accessSecretsManager: true, + }); + } const groups$ = this.organization$.pipe( switchMap((organization) => @@ -206,25 +216,52 @@ export class MemberDialogComponent implements OnDestroy { collections: this.collectionAdminService.getAll(this.params.organizationId), userDetails: userDetails$, groups: groups$, + flexibleCollectionsV1Enabled: this.configService.getFeatureFlag$( + FeatureFlag.FlexibleCollectionsV1, + false, + ), }) .pipe(takeUntil(this.destroy$)) - .subscribe(({ organization, collections, userDetails, groups }) => { - this.setFormValidators(organization); + .subscribe( + ({ organization, collections, userDetails, groups, flexibleCollectionsV1Enabled }) => { + this.setFormValidators(organization); - this.collectionAccessItems = [].concat( - collections.map((c) => mapCollectionToAccessItemView(c)), - ); + // Groups tab: populate available groups + this.groupAccessItems = [].concat( + groups.map((g) => mapGroupToAccessItemView(g)), + ); - this.groupAccessItems = [].concat( - groups.map((g) => mapGroupToAccessItemView(g)), - ); + // Collections tab: Populate all available collections (including current user access where applicable) + this.collectionAccessItems = collections + .map((c) => + mapCollectionToAccessItemView( + c, + organization, + flexibleCollectionsV1Enabled, + userDetails == null + ? undefined + : c.users.find((access) => access.id === userDetails.id), + ), + ) + // But remove collections that we can't assign access to, unless the user is already assigned + .filter( + (item) => + !item.readonly || userDetails?.collections.some((access) => access.id == item.id), + ); - if (this.params.organizationUserId) { - this.loadOrganizationUser(userDetails, groups, collections); - } + if (userDetails != null) { + this.loadOrganizationUser( + userDetails, + groups, + collections, + organization, + flexibleCollectionsV1Enabled, + ); + } - this.loading = false; - }); + this.loading = false; + }, + ); } private setFormValidators(organization: Organization) { @@ -246,7 +283,9 @@ export class MemberDialogComponent implements OnDestroy { private loadOrganizationUser( userDetails: OrganizationUserAdminView, groups: GroupView[], - collections: CollectionView[], + collections: CollectionAdminView[], + organization: Organization, + flexibleCollectionsV1Enabled: boolean, ) { if (!userDetails) { throw new Error("Could not find user to edit."); @@ -295,13 +334,22 @@ export class MemberDialogComponent implements OnDestroy { }), ); + // Populate additional collection access via groups (rendered as separate rows from user access) this.collectionAccessItems = this.collectionAccessItems.concat( collectionsFromGroups.map(({ collection, accessSelection, group }) => - mapCollectionToAccessItemView(collection, accessSelection, group), + mapCollectionToAccessItemView( + collection, + organization, + flexibleCollectionsV1Enabled, + accessSelection, + group, + ), ), ); - const accessSelections = mapToAccessSelections(userDetails); + // Set current collections and groups the user has access to (excluding collections the current user doesn't have + // permissions to change - they are included as readonly via the CollectionAccessItems) + const accessSelections = mapToAccessSelections(userDetails, this.collectionAccessItems); const groupAccessSelections = mapToGroupAccessSelections(userDetails.groups); this.formGroup.removeControl("emails"); @@ -573,6 +621,8 @@ export class MemberDialogComponent implements OnDestroy { function mapCollectionToAccessItemView( collection: CollectionView, + organization: Organization, + flexibleCollectionsV1Enabled: boolean, accessSelection?: CollectionAccessSelectionView, group?: GroupView, ): AccessItemView { @@ -581,7 +631,8 @@ function mapCollectionToAccessItemView( id: group ? `${collection.id}-${group.id}` : collection.id, labelName: collection.name, listName: collection.name, - readonly: group !== undefined, + readonly: + group !== undefined || !collection.canEdit(organization, flexibleCollectionsV1Enabled), readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined, viaGroupName: group?.name, }; @@ -596,16 +647,23 @@ function mapGroupToAccessItemView(group: GroupView): AccessItemView { }; } -function mapToAccessSelections(user: OrganizationUserAdminView): AccessItemValue[] { +function mapToAccessSelections( + user: OrganizationUserAdminView, + items: AccessItemView[], +): AccessItemValue[] { if (user == undefined) { return []; } - return [].concat( - user.collections.map((selection) => ({ - id: selection.id, - type: AccessItemType.Collection, - permission: convertToPermission(selection), - })), + + return ( + user.collections + // The FormControl value only represents editable collection access - exclude readonly access selections + .filter((selection) => !items.find((item) => item.id == selection.id).readonly) + .map((selection) => ({ + id: selection.id, + type: AccessItemType.Collection, + permission: convertToPermission(selection), + })) ); } diff --git a/apps/web/src/app/admin-console/organizations/members/people.component.ts b/apps/web/src/app/admin-console/organizations/members/people.component.ts index 8a303ddfe5..0df247d7b0 100644 --- a/apps/web/src/app/admin-console/organizations/members/people.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/people.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, ViewChild, ViewContainerRef } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { combineLatest, @@ -9,7 +9,6 @@ import { map, Observable, shareReplay, - Subject, switchMap, takeUntil, } from "rxjs"; @@ -38,6 +37,7 @@ import { import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; +import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service"; import { ProductType } from "@bitwarden/common/enums"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -52,7 +52,6 @@ import { Collection } from "@bitwarden/common/vault/models/domain/collection"; import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response"; import { DialogService, SimpleDialogOptions } from "@bitwarden/components"; -import { flagEnabled } from "../../../../utils/flags"; import { openEntityEventsDialog } from "../../../admin-console/organizations/manage/entity-events.component"; import { BasePeopleComponent } from "../../common/base.people.component"; import { GroupService } from "../core"; @@ -74,10 +73,7 @@ import { ResetPasswordComponent } from "./components/reset-password.component"; selector: "app-org-people", templateUrl: "people.component.html", }) -export class PeopleComponent - extends BasePeopleComponent - implements OnInit, OnDestroy -{ +export class PeopleComponent extends BasePeopleComponent { @ViewChild("groupsTemplate", { read: ViewContainerRef, static: true }) groupsModalRef: ViewContainerRef; @ViewChild("confirmTemplate", { read: ViewContainerRef, static: true }) @@ -98,9 +94,9 @@ export class PeopleComponent organization: Organization; status: OrganizationUserStatusType = null; orgResetPasswordPolicyEnabled = false; + orgIsOnSecretsManagerStandalone = false; protected canUseSecretsManager$: Observable; - private destroy$ = new Subject(); constructor( apiService: ApiService, @@ -125,6 +121,7 @@ export class PeopleComponent private groupService: GroupService, private collectionService: CollectionService, organizationManagementPreferencesService: OrganizationManagementPreferencesService, + private organizationBillingService: OrganizationBillingService, ) { super( apiService, @@ -148,9 +145,7 @@ export class PeopleComponent shareReplay({ refCount: true, bufferSize: 1 }), ); - this.canUseSecretsManager$ = organization$.pipe( - map((org) => org.useSecretsManager && flagEnabled("secretsManager")), - ); + this.canUseSecretsManager$ = organization$.pipe(map((org) => org.useSecretsManager)); const policies$ = organization$.pipe( switchMap((organization) => { @@ -195,6 +190,11 @@ export class PeopleComponent .find((p) => p.organizationId === this.organization.id); this.orgResetPasswordPolicyEnabled = resetPasswordPolicy?.enabled; + this.orgIsOnSecretsManagerStandalone = + await this.organizationBillingService.isOnSecretsManagerStandalone( + this.organization.id, + ); + await this.load(); this.searchText = qParams.search; @@ -213,8 +213,7 @@ export class PeopleComponent } ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); + super.ngOnDestroy(); } async load() { @@ -455,6 +454,7 @@ export class PeopleComponent organizationUserId: user != null ? user.id : null, allOrganizationUserEmails: this.allUsers?.map((user) => user.email) ?? [], usesKeyConnector: user?.usesKeyConnector, + isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone, initialTab: initialTab, numConfirmedMembers: this.confirmedCount, }, diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.html b/apps/web/src/app/admin-console/organizations/settings/account.component.html index 7035b976ca..082fe7eb80 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.html +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.html @@ -20,10 +20,6 @@ {{ "billingEmail" | i18n }} - - {{ "businessName" | i18n }} - -
diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.ts b/apps/web/src/app/admin-console/organizations/settings/account.component.ts index b218e680e3..1ce05f7a30 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.ts @@ -65,10 +65,6 @@ export class AccountComponent { { value: "", disabled: true }, { validators: [Validators.required, Validators.email, Validators.maxLength(256)] }, ), - businessName: this.formBuilder.control( - { value: "", disabled: true }, - { validators: [Validators.maxLength(50)] }, - ), }); protected collectionManagementFormGroup = this.formBuilder.group({ @@ -124,7 +120,6 @@ export class AccountComponent { // Update disabled states - reactive forms prefers not using disabled attribute if (!this.selfHosted) { this.formGroup.get("orgName").enable(); - this.formGroup.get("businessName").enable(); this.collectionManagementFormGroup.get("limitCollectionCreationDeletion").enable(); this.collectionManagementFormGroup.get("allowAdminAccessToAllCollectionItems").enable(); } @@ -143,7 +138,6 @@ export class AccountComponent { this.formGroup.patchValue({ orgName: this.org.name, billingEmail: this.org.billingEmail, - businessName: this.org.businessName, }); this.collectionManagementFormGroup.patchValue({ limitCollectionCreationDeletion: this.org.limitCollectionCreationDeletion, @@ -168,7 +162,6 @@ export class AccountComponent { const request = new OrganizationUpdateRequest(); request.name = this.formGroup.value.orgName; - request.businessName = this.formGroup.value.businessName; request.billingEmail = this.formGroup.value.billingEmail; // Backfill pub/priv key if necessary diff --git a/apps/web/src/app/admin-console/organizations/settings/settings.component.html b/apps/web/src/app/admin-console/organizations/settings/settings.component.html deleted file mode 100644 index 47592df378..0000000000 --- a/apps/web/src/app/admin-console/organizations/settings/settings.component.html +++ /dev/null @@ -1,88 +0,0 @@ - diff --git a/apps/web/src/app/admin-console/organizations/settings/settings.component.ts b/apps/web/src/app/admin-console/organizations/settings/settings.component.ts deleted file mode 100644 index ab25829d19..0000000000 --- a/apps/web/src/app/admin-console/organizations/settings/settings.component.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Component, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { Observable, switchMap } from "rxjs"; - -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"; - -@Component({ - selector: "app-org-settings", - templateUrl: "settings.component.html", -}) -export class SettingsComponent implements OnInit { - organization$: Observable; - FeatureFlag = FeatureFlag; - - constructor( - private route: ActivatedRoute, - private organizationService: OrganizationService, - ) {} - - ngOnInit() { - this.organization$ = this.route.params.pipe( - switchMap((params) => this.organizationService.get$(params.organizationId)), - ); - } -} diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html index 16a24781df..f4c9c840ef 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html @@ -22,7 +22,7 @@ - + {{ selectorLabelText }}
@@ -139,7 +139,7 @@ - - - - diff --git a/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts b/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts index 4cbdbf3864..b228a4d135 100644 --- a/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts +++ b/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts @@ -1,12 +1,7 @@ -import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; -import { Component, Inject } from "@angular/core"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; - +import { UserVerificationDialogComponent } from "@bitwarden/auth/angular"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { Verification } from "@bitwarden/common/auth/types/verification"; 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"; @@ -19,63 +14,58 @@ interface EnrollMasterPasswordResetData { organization: Organization; } -@Component({ - selector: "app-enroll-master-password-reset", - templateUrl: "enroll-master-password-reset.component.html", -}) export class EnrollMasterPasswordReset { - protected organization: Organization; + constructor() {} - protected formGroup = new FormGroup({ - verification: new FormControl(null, Validators.required), - }); - - constructor( - private dialogRef: DialogRef, - @Inject(DIALOG_DATA) protected data: EnrollMasterPasswordResetData, - private resetPasswordService: OrganizationUserResetPasswordService, - private userVerificationService: UserVerificationService, - private platformUtilsService: PlatformUtilsService, - private i18nService: I18nService, - private syncService: SyncService, - private logService: LogService, - private organizationUserService: OrganizationUserService, + static async open( + dialogService: DialogService, + data: EnrollMasterPasswordResetData, + resetPasswordService: OrganizationUserResetPasswordService, + organizationUserService: OrganizationUserService, + platformUtilsService: PlatformUtilsService, + i18nService: I18nService, + syncService: SyncService, + logService: LogService, ) { - this.organization = data.organization; - } + const result = await UserVerificationDialogComponent.open(dialogService, { + title: "enrollAccountRecovery", + calloutOptions: { + text: "resetPasswordEnrollmentWarning", + type: "warning", + }, + }); - submit = async () => { - try { - await this.userVerificationService - .buildRequest( - this.formGroup.value.verification, - OrganizationUserResetPasswordEnrollmentRequest, - ) - .then(async (request) => { - // Create request and execute enrollment - request.resetPasswordKey = await this.resetPasswordService.buildRecoveryKey( - this.organization.id, - ); - await this.organizationUserService.putOrganizationUserResetPasswordEnrollment( - this.organization.id, - this.organization.userId, - request, - ); - - await this.syncService.fullSync(true); - }); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("enrollPasswordResetSuccess"), - ); - this.dialogRef.close(); - } catch (e) { - this.logService.error(e); + // Handle the result of the dialog based on user action and verification success + if (result.userAction === "cancel") { + return; } - }; - static open(dialogService: DialogService, data: EnrollMasterPasswordResetData) { - return dialogService.open(EnrollMasterPasswordReset, { data }); + // User confirmed the dialog so check verification success + if (!result.verificationSuccess) { + // verification failed + return; + } + + // Verification succeeded + try { + // This object is missing most of the properties in the + // `OrganizationUserResetPasswordEnrollmentRequest()`, but those + // properties don't carry over to the server model anyway and are + // never used by this flow. + const request = new OrganizationUserResetPasswordEnrollmentRequest(); + request.resetPasswordKey = await resetPasswordService.buildRecoveryKey(data.organization.id); + + await organizationUserService.putOrganizationUserResetPasswordEnrollment( + data.organization.id, + data.organization.userId, + request, + ); + + platformUtilsService.showToast("success", null, i18nService.t("enrollPasswordResetSuccess")); + + await syncService.fullSync(true); + } catch (e) { + logService.error(e); + } } } diff --git a/apps/web/src/app/admin-console/organizations/users/organization-user.module.ts b/apps/web/src/app/admin-console/organizations/users/organization-user.module.ts deleted file mode 100644 index 30e2b5abe7..0000000000 --- a/apps/web/src/app/admin-console/organizations/users/organization-user.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ScrollingModule } from "@angular/cdk/scrolling"; -import { NgModule } from "@angular/core"; - -import { UserVerificationModule } from "../../../auth/shared/components/user-verification"; -import { LooseComponentsModule, SharedModule } from "../../../shared"; - -import { EnrollMasterPasswordReset } from "./enroll-master-password-reset.component"; - -@NgModule({ - imports: [SharedModule, ScrollingModule, LooseComponentsModule, UserVerificationModule], - declarations: [EnrollMasterPasswordReset], - exports: [EnrollMasterPasswordReset], -}) -export class OrganizationUserModule {} diff --git a/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.html b/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.html new file mode 100644 index 0000000000..a287a537a4 --- /dev/null +++ b/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.html @@ -0,0 +1,34 @@ +
+
+
+

{{ "deleteProvider" | i18n }}

+
+
+ {{ "deleteProviderWarning" | i18n }} +

+ {{ name }} +

+

{{ "deleteProviderRecoverConfirmDesc" | i18n }}

+
+
+ + + {{ "cancel" | i18n }} + +
+
+
+
+
+
diff --git a/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts b/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts new file mode 100644 index 0000000000..0550820cda --- /dev/null +++ b/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts @@ -0,0 +1,61 @@ +import { Component, OnInit } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; + +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; +import { ProviderVerifyRecoverDeleteRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-verify-recover-delete.request"; +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"; + +@Component({ + selector: "app-verify-recover-delete-provider", + templateUrl: "verify-recover-delete-provider.component.html", +}) +// eslint-disable-next-line rxjs-angular/prefer-takeuntil +export class VerifyRecoverDeleteProviderComponent implements OnInit { + name: string; + formPromise: Promise; + + private providerId: string; + private token: string; + + constructor( + private router: Router, + private providerApiService: ProviderApiServiceAbstraction, + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, + private route: ActivatedRoute, + private logService: LogService, + ) {} + + async ngOnInit() { + const qParams = await firstValueFrom(this.route.queryParams); + if (qParams.providerId != null && qParams.token != null && qParams.name != null) { + this.providerId = qParams.providerId; + this.token = qParams.token; + this.name = qParams.name; + } else { + await this.router.navigate(["/"]); + } + } + + async submit() { + try { + const request = new ProviderVerifyRecoverDeleteRequest(this.token); + this.formPromise = this.providerApiService.providerRecoverDeleteToken( + this.providerId, + request, + ); + await this.formPromise; + this.platformUtilsService.showToast( + "success", + this.i18nService.t("providerDeleted"), + this.i18nService.t("providerDeletedDesc"), + ); + await this.router.navigate(["/"]); + } catch (e) { + this.logService.error(e); + } + } +} diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 32f4ee67e2..1da2d94c15 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -1,9 +1,7 @@ import { DOCUMENT } from "@angular/common"; -import { Component, Inject, NgZone, OnDestroy, OnInit, SecurityContext } from "@angular/core"; -import { DomSanitizer } from "@angular/platform-browser"; +import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router } from "@angular/router"; import * as jq from "jquery"; -import { IndividualConfig, ToastrService } from "ngx-toastr"; import { Subject, switchMap, takeUntil, timer } from "rxjs"; import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; @@ -29,7 +27,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PolicyListService } from "./admin-console/core/policy-list.service"; import { @@ -68,14 +66,13 @@ export class AppComponent implements OnDestroy, OnInit { private cipherService: CipherService, private authService: AuthService, private router: Router, - private toastrService: ToastrService, + private toastService: ToastService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private ngZone: NgZone, private vaultTimeoutService: VaultTimeoutService, private cryptoService: CryptoService, private collectionService: CollectionService, - private sanitizer: DomSanitizer, private searchService: SearchService, private notificationsService: NotificationsService, private stateService: StateService, @@ -209,7 +206,7 @@ export class AppComponent implements OnDestroy, OnInit { break; } case "showToast": - this.showToast(message); + this.toastService._showToast(message); break; case "convertAccountToKeyConnector": // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -274,7 +271,6 @@ export class AppComponent implements OnDestroy, OnInit { this.cipherService.clear(userId), this.folderService.clear(userId), this.collectionService.clear(userId), - this.policyService.clear(userId), this.passwordGenerationService.clear(), this.biometricStateService.logout(userId as UserId), this.paymentMethodWarningService.clear(), @@ -282,7 +278,7 @@ export class AppComponent implements OnDestroy, OnInit { await this.stateEventRunnerService.handleEvent("logout", userId as UserId); - this.searchService.clearIndex(); + await this.searchService.clearIndex(); this.authService.logOut(async () => { if (expired) { this.platformUtilsService.showToast( @@ -328,34 +324,6 @@ export class AppComponent implements OnDestroy, OnInit { }, IdleTimeout); } - private showToast(msg: any) { - let message = ""; - - const options: Partial = {}; - - if (typeof msg.text === "string") { - message = msg.text; - } else if (msg.text.length === 1) { - message = msg.text[0]; - } else { - msg.text.forEach( - (t: string) => - (message += "

" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "

"), - ); - options.enableHtml = true; - } - if (msg.options != null) { - if (msg.options.trustedHtml === true) { - options.enableHtml = true; - } - if (msg.options.timeout != null && msg.options.timeout > 0) { - options.timeOut = msg.options.timeout; - } - } - - this.toastrService.show(message, msg.title, options, "toast-" + msg.type); - } - private idleStateChanged() { if (this.isIdle) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts index 09c7bf9ace..0997f18864 100644 --- a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts +++ b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -9,7 +10,6 @@ import { EncryptionType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { Send } from "@bitwarden/common/tools/send/models/domain/send"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; @@ -22,6 +22,10 @@ import { Folder } from "@bitwarden/common/vault/models/domain/folder"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { + FakeAccountService, + mockAccountServiceWith, +} from "../../../../../../libs/common/spec/fake-account-service"; import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; import { StateService } from "../../core"; import { EmergencyAccessService } from "../emergency-access"; @@ -46,8 +50,10 @@ describe("KeyRotationService", () => { const mockUserId = Utils.newGuid() as UserId; const mockAccountService: FakeAccountService = mockAccountServiceWith(mockUserId); + let mockMasterPasswordService: FakeMasterPasswordService = new FakeMasterPasswordService(); beforeAll(() => { + mockMasterPasswordService = new FakeMasterPasswordService(); mockApiService = mock(); mockCipherService = mock(); mockFolderService = mock(); @@ -61,6 +67,7 @@ describe("KeyRotationService", () => { mockConfigService = mock(); keyRotationService = new UserKeyRotationService( + mockMasterPasswordService, mockApiService, mockCipherService, mockFolderService, @@ -174,7 +181,10 @@ describe("KeyRotationService", () => { it("saves the master key in state after creation", async () => { await keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword"); - expect(mockCryptoService.setMasterKey).toHaveBeenCalledWith("mockMasterKey" as any); + expect(mockMasterPasswordService.mock.setMasterKey).toHaveBeenCalledWith( + "mockMasterKey" as any, + mockUserId, + ); }); it("uses legacy rotation if feature flag is off", async () => { diff --git a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts index 03bc604b4d..f5812d341a 100644 --- a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts @@ -3,6 +3,7 @@ import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -25,6 +26,7 @@ import { UserKeyRotationApiService } from "./user-key-rotation-api.service"; @Injectable() export class UserKeyRotationService { constructor( + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private apiService: UserKeyRotationApiService, private cipherService: CipherService, private folderService: FolderService, @@ -61,7 +63,8 @@ export class UserKeyRotationService { } // Set master key again in case it was lost (could be lost on refresh) - await this.cryptoService.setMasterKey(masterKey); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setMasterKey(masterKey, userId); const [newUserKey, newEncUserKey] = await this.cryptoService.makeUserKey(masterKey); if (!newUserKey || !newEncUserKey) { diff --git a/apps/web/src/app/auth/lock.component.ts b/apps/web/src/app/auth/lock.component.ts index a1d4724396..021bf0f9df 100644 --- a/apps/web/src/app/auth/lock.component.ts +++ b/apps/web/src/app/auth/lock.component.ts @@ -1,80 +1,12 @@ -import { Component, NgZone } from "@angular/core"; -import { Router } from "@angular/router"; +import { Component } from "@angular/core"; import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component"; -import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; -import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; -import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; -import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; -import { DialogService } from "@bitwarden/components"; @Component({ selector: "app-lock", templateUrl: "lock.component.html", }) export class LockComponent extends BaseLockComponent { - constructor( - router: Router, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - messagingService: MessagingService, - cryptoService: CryptoService, - vaultTimeoutService: VaultTimeoutService, - vaultTimeoutSettingsService: VaultTimeoutSettingsService, - environmentService: EnvironmentService, - stateService: StateService, - apiService: ApiService, - logService: LogService, - ngZone: NgZone, - policyApiService: PolicyApiServiceAbstraction, - policyService: InternalPolicyService, - passwordStrengthService: PasswordStrengthServiceAbstraction, - dialogService: DialogService, - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, - userVerificationService: UserVerificationService, - pinCryptoService: PinCryptoServiceAbstraction, - biometricStateService: BiometricStateService, - accountService: AccountService, - ) { - super( - router, - i18nService, - platformUtilsService, - messagingService, - cryptoService, - vaultTimeoutService, - vaultTimeoutSettingsService, - environmentService, - stateService, - apiService, - logService, - ngZone, - policyApiService, - policyService, - passwordStrengthService, - dialogService, - deviceTrustCryptoService, - userVerificationService, - pinCryptoService, - biometricStateService, - accountService, - ); - } - async ngOnInit() { await super.ngOnInit(); this.onSuccessfulSubmit = async () => { diff --git a/apps/web/src/app/auth/settings/account/change-avatar.component.html b/apps/web/src/app/auth/settings/account/change-avatar.component.html index 38e35399ae..3a974241d5 100644 --- a/apps/web/src/app/auth/settings/account/change-avatar.component.html +++ b/apps/web/src/app/auth/settings/account/change-avatar.component.html @@ -43,10 +43,10 @@ (click)="showCustomPicker()" title="{{ 'customColor' | i18n }}" [ngClass]="{ - '!tw-outline-[3px] tw-outline-primary-500 hover:tw-outline-[3px] hover:tw-outline-primary-500': + '!tw-outline-[3px] tw-outline-primary-600 hover:tw-outline-[3px] hover:tw-outline-primary-600': customColorSelected }" - class="tw-outline-solid tw-bg-white tw-relative tw-flex tw-h-24 tw-w-24 tw-cursor-pointer tw-place-content-center tw-content-center tw-justify-center tw-rounded-full tw-border tw-border-solid tw-border-secondary-500 tw-outline tw-outline-0 tw-outline-offset-1 hover:tw-outline-1 hover:tw-outline-primary-300 focus:tw-outline-2 focus:tw-outline-primary-500" + class="tw-outline-solid tw-bg-white tw-relative tw-flex tw-h-24 tw-w-24 tw-cursor-pointer tw-place-content-center tw-content-center tw-justify-center tw-rounded-full tw-border tw-border-solid tw-border-secondary-600 tw-outline tw-outline-0 tw-outline-offset-1 hover:tw-outline-1 hover:tw-outline-primary-300 focus:tw-outline-2 focus:tw-outline-primary-600" [style.background-color]="customColor$ | async" > {{ "dangerZone" | i18n }} -
+

{{ "dangerZoneDesc" | i18n }}

diff --git a/apps/web/src/app/auth/settings/verify-email.component.html b/apps/web/src/app/auth/settings/verify-email.component.html index 6fd2128651..ccad78348c 100644 --- a/apps/web/src/app/auth/settings/verify-email.component.html +++ b/apps/web/src/app/auth/settings/verify-email.component.html @@ -1,5 +1,5 @@ -
-
+
+
{{ "verifyEmail" | i18n }}
diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-passkey-failed.icon.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-passkey-failed.icon.ts index 39a2389c5a..65902a64c9 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-passkey-failed.icon.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-passkey-failed.icon.ts @@ -2,27 +2,27 @@ import { svgIcon } from "@bitwarden/components"; export const CreatePasskeyFailedIcon = svgIcon` - - - - - - - - - `; diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-passkey.icon.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-passkey.icon.ts index c0e984bbee..79ba4021b5 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-passkey.icon.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-passkey.icon.ts @@ -2,25 +2,25 @@ import { svgIcon } from "@bitwarden/components"; export const CreatePasskeyIcon = svgIcon` - - - - - - - - `; diff --git a/apps/web/src/app/auth/sso.component.ts b/apps/web/src/app/auth/sso.component.ts index cdd979aa89..e120b2749f 100644 --- a/apps/web/src/app/auth/sso.component.ts +++ b/apps/web/src/app/auth/sso.component.ts @@ -10,6 +10,8 @@ import { import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction"; import { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain-sso-details.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { HttpStatusCode } from "@bitwarden/common/enums"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; @@ -46,6 +48,8 @@ export class SsoComponent extends BaseSsoComponent { private validationService: ValidationService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, configService: ConfigService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, ) { super( ssoLoginService, @@ -62,6 +66,8 @@ export class SsoComponent extends BaseSsoComponent { logService, userDecryptionOptionsService, configService, + masterPasswordService, + accountService, ); this.redirectUri = window.location.origin + "/sso-connector.html"; this.clientId = "web"; diff --git a/apps/web/src/app/auth/trial-initiation/vertical-stepper/vertical-step-content.component.html b/apps/web/src/app/auth/trial-initiation/vertical-stepper/vertical-step-content.component.html index 622a9f6d92..06b1dc7c51 100644 --- a/apps/web/src/app/auth/trial-initiation/vertical-stepper/vertical-step-content.component.html +++ b/apps/web/src/app/auth/trial-initiation/vertical-stepper/vertical-step-content.component.html @@ -14,7 +14,7 @@ class="tw-mr-3.5 tw-w-9 tw-rounded-full tw-font-bold tw-leading-9" *ngIf="!step.completed" [ngClass]="{ - 'tw-bg-primary-500 tw-text-contrast': selected, + 'tw-bg-primary-600 tw-text-contrast': selected, 'tw-bg-secondary-300 tw-text-main': !selected && !disabled && step.editable, 'tw-bg-transparent tw-text-muted': disabled }" @@ -22,7 +22,7 @@ {{ stepNumber }} diff --git a/apps/web/src/app/auth/two-factor.component.ts b/apps/web/src/app/auth/two-factor.component.ts index 65bf1dba58..eed84b91f1 100644 --- a/apps/web/src/app/auth/two-factor.component.ts +++ b/apps/web/src/app/auth/two-factor.component.ts @@ -10,6 +10,8 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -50,6 +52,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, configService: ConfigService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, @Inject(WINDOW) protected win: Window, ) { super( @@ -69,6 +73,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest userDecryptionOptionsService, ssoLoginService, configService, + masterPasswordService, + accountService, ); this.onSuccessfulLoginNavigate = this.goAfterLogIn; } diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts index 0c5f1d706b..bd138cad29 100644 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts +++ b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts @@ -103,7 +103,8 @@ export class TrialBillingStepComponent implements OnInit { planDescription, }); - this.messagingService.send("organizationCreated", organizationId); + // TODO: No one actually listening to this? + this.messagingService.send("organizationCreated", { organizationId }); } protected changedCountry() { diff --git a/apps/web/src/app/billing/individual/billing-history-view.component.html b/apps/web/src/app/billing/individual/billing-history-view.component.html index 1032558f5f..2491fc42c7 100644 --- a/apps/web/src/app/billing/individual/billing-history-view.component.html +++ b/apps/web/src/app/billing/individual/billing-history-view.component.html @@ -1,13 +1,12 @@ -
-

+
+

{{ "billingHistory" | i18n }} -

+

- {{ "loading" | i18n }} + {{ "loading" | i18n }} 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 874983df84..380116e81b 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.html +++ b/apps/web/src/app/billing/individual/user-subscription.component.html @@ -170,8 +170,8 @@
-
-
-
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 7d8c3a0f18..fa21317c18 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.ts +++ b/apps/web/src/app/billing/individual/user-subscription.component.ts @@ -12,6 +12,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; +import { + AdjustStorageDialogResult, + openAdjustStorageDialog, +} from "../shared/adjust-storage.component"; import { OffboardingSurveyDialogResultType, openOffboardingSurvey, @@ -24,7 +28,6 @@ export class UserSubscriptionComponent implements OnInit { loading = false; firstLoaded = false; adjustStorageAdd = true; - showAdjustStorage = false; showUpdateLicense = false; sub: SubscriptionResponse; selfHosted = false; @@ -144,19 +147,20 @@ export class UserSubscriptionComponent implements OnInit { } } - adjustStorage(add: boolean) { - this.adjustStorageAdd = add; - this.showAdjustStorage = true; - } - - closeStorage(load: boolean) { - this.showAdjustStorage = false; - if (load) { - // 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.load(); - } - } + adjustStorage = (add: boolean) => { + return async () => { + const dialogRef = openAdjustStorageDialog(this.dialogService, { + data: { + storageGbPrice: 4, + add: add, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === AdjustStorageDialogResult.Adjusted) { + await this.load(); + } + }; + }; get subscriptionMarkedForCancel() { return ( diff --git a/apps/web/src/app/billing/organizations/change-plan.component.html b/apps/web/src/app/billing/organizations/change-plan.component.html index b9a15be5ea..a25dde4fd3 100644 --- a/apps/web/src/app/billing/organizations/change-plan.component.html +++ b/apps/web/src/app/billing/organizations/change-plan.component.html @@ -1,10 +1,18 @@ -
-
- -

{{ "changeBillingPlan" | i18n }}

-

{{ "changeBillingPlanUpgrade" | i18n }}

+
+
+ +

{{ "changeBillingPlan" | i18n }}

+

{{ "changeBillingPlanUpgrade" | i18n }}

- {{ "loading" | i18n }} + {{ "loading" | i18n }} diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index 1242018673..23d48d93be 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -15,6 +15,7 @@ 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 { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { OrganizationCreateRequest } from "@bitwarden/common/admin-console/models/request/organization-create.request"; @@ -47,7 +48,7 @@ interface OnSuccessArgs { organizationId: string; } -const Allowed2020PlanTypes = [ +const Allowed2020PlansForLegacyProviders = [ PlanType.TeamsMonthly2020, PlanType.TeamsAnnually2020, PlanType.EnterpriseAnnually2020, @@ -116,7 +117,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { additionalStorage: [0, [Validators.min(0), Validators.max(99)]], additionalSeats: [0, [Validators.min(0), Validators.max(100000)]], clientOwnerEmail: ["", [Validators.email]], - businessName: [""], plan: [this.plan], product: [this.product], secretsManager: this.secretsManagerSubscription, @@ -144,6 +144,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { private messagingService: MessagingService, private formBuilder: FormBuilder, private organizationApiService: OrganizationApiServiceAbstraction, + private providerApiService: ProviderApiServiceAbstraction, ) { this.selfHosted = platformUtilsService.isSelfHost(); } @@ -179,7 +180,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { if (this.hasProvider) { this.formGroup.controls.businessOwned.setValue(true); this.changedOwnedBusiness(); - this.provider = await this.apiService.getProvider(this.providerId); + this.provider = await this.providerApiService.getProvider(this.providerId); const providerDefaultPlan = this.passwordManagerPlans.find( (plan) => plan.type === PlanType.TeamsAnnually, ); @@ -278,7 +279,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { (!this.currentPlan || this.currentPlan.upgradeSortOrder < plan.upgradeSortOrder) && (!this.hasProvider || plan.product !== ProductType.TeamsStarter) && ((!this.isProviderQualifiedFor2020Plan() && this.planIsEnabled(plan)) || - (this.isProviderQualifiedFor2020Plan() && Allowed2020PlanTypes.includes(plan.type))), + (this.isProviderQualifiedFor2020Plan() && + Allowed2020PlansForLegacyProviders.includes(plan.type))), ); result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder); @@ -293,7 +295,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { (plan) => plan.product === selectedProductType && ((!this.isProviderQualifiedFor2020Plan() && this.planIsEnabled(plan)) || - (this.isProviderQualifiedFor2020Plan() && Allowed2020PlanTypes.includes(plan.type))), + (this.isProviderQualifiedFor2020Plan() && + Allowed2020PlansForLegacyProviders.includes(plan.type))), ) || []; result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder); @@ -584,7 +587,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.formPromise = doSubmit(); const organizationId = await this.formPromise; this.onSuccess.emit({ organizationId: organizationId }); - this.messagingService.send("organizationCreated", organizationId); + // TODO: No one actually listening to this message? + this.messagingService.send("organizationCreated", { organizationId }); } catch (e) { this.logService.error(e); } @@ -592,9 +596,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { private async updateOrganization(orgId: string) { const request = new OrganizationUpgradeRequest(); - request.businessName = this.formGroup.controls.businessOwned.value - ? this.formGroup.controls.businessName.value - : null; request.additionalSeats = this.formGroup.controls.additionalSeats.value; request.additionalStorageGb = this.formGroup.controls.additionalStorage.value; request.premiumAccessAddon = @@ -652,9 +653,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { request.paymentToken = tokenResult[0]; request.paymentMethodType = tokenResult[1]; - request.businessName = this.formGroup.controls.businessOwned.value - ? this.formGroup.controls.businessName.value - : null; request.additionalSeats = this.formGroup.controls.additionalSeats.value; request.additionalStorageGb = this.formGroup.controls.additionalStorage.value; request.premiumAccessAddon = 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 b4fac65854..16641c0d52 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 @@ -175,23 +175,24 @@
-
- -
-
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 0810f79b8e..9326359bd8 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 @@ -18,6 +18,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; +import { + AdjustStorageDialogResult, + openAdjustStorageDialog, +} from "../shared/adjust-storage.component"; import { OffboardingSurveyDialogResultType, openOffboardingSurvey, @@ -36,8 +40,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy userOrg: Organization; showChangePlan = false; showDownloadLicense = false; - adjustStorageAdd = true; - showAdjustStorage = false; hasBillingSyncToken: boolean; showAdjustSecretsManager = false; showSecretsManagerSubscribe = false; @@ -241,6 +243,8 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy return ( this.sub.planType === PlanType.EnterpriseAnnually || this.sub.planType === PlanType.EnterpriseMonthly || + this.sub.planType === PlanType.EnterpriseAnnually2023 || + this.sub.planType === PlanType.EnterpriseMonthly2023 || this.sub.planType === PlanType.EnterpriseAnnually2020 || this.sub.planType === PlanType.EnterpriseMonthly2020 || this.sub.planType === PlanType.EnterpriseAnnually2019 || @@ -254,6 +258,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy } else if ( this.sub.planType === PlanType.FamiliesAnnually || this.sub.planType === PlanType.FamiliesAnnually2019 || + this.sub.planType === PlanType.TeamsStarter2023 || this.sub.planType === PlanType.TeamsStarter ) { if (this.isSponsoredSubscription) { @@ -358,19 +363,22 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy this.load(); } - adjustStorage(add: boolean) { - this.adjustStorageAdd = add; - this.showAdjustStorage = true; - } - - closeStorage(load: boolean) { - this.showAdjustStorage = false; - if (load) { - // 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.load(); - } - } + adjustStorage = (add: boolean) => { + return async () => { + const dialogRef = openAdjustStorageDialog(this.dialogService, { + data: { + storageGbPrice: this.storageGbPrice, + add: add, + organizationId: this.organizationId, + interval: this.billingInterval, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === AdjustStorageDialogResult.Adjusted) { + await this.load(); + } + }; + }; removeSponsorship = async () => { const confirmed = await this.dialogService.openSimpleDialog({ diff --git a/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.html b/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.html index c10ef9af28..4c8ea28284 100644 --- a/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.html +++ b/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.html @@ -33,7 +33,7 @@ - {{ "additionalServiceAccounts" | i18n }} + {{ "additionalMachineAccounts" | i18n }}
- {{ "includedServiceAccounts" | i18n: options.baseServiceAccountCount }} - {{ "addAdditionalServiceAccounts" | i18n: (monthlyServiceAccountPrice | currency: "$") }} + {{ "includedMachineAccounts" | i18n: options.baseServiceAccountCount }} + {{ "addAdditionalMachineAccounts" | i18n: (monthlyServiceAccountPrice | currency: "$") }}
{{ "total" | i18n }}: @@ -56,7 +56,7 @@ - {{ "limitServiceAccounts" | i18n }} + {{ "limitMachineAccounts" | i18n }} - {{ "limitServiceAccountsDesc" | i18n }} + {{ "limitMachineAccountsDesc" | i18n }} - {{ "serviceAccountLimit" | i18n }} + {{ "machineAccountLimit" | i18n }}
- {{ "includedServiceAccounts" | i18n: options.baseServiceAccountCount }} + {{ "includedMachineAccounts" | i18n: options.baseServiceAccountCount }}
- {{ "maxServiceAccountCost" | i18n }}: + {{ "maxMachineAccountCost" | i18n }}: {{ maxAdditionalServiceAccounts }} × {{ options.additionalServiceAccountPrice | currency: "$" }} = {{ maxServiceAccountTotalCost | currency: "$" }} / {{ options.interval | i18n }} diff --git a/apps/web/src/app/billing/organizations/subscription-status.component.html b/apps/web/src/app/billing/organizations/subscription-status.component.html index 4bb2c91b85..99673a228e 100644 --- a/apps/web/src/app/billing/organizations/subscription-status.component.html +++ b/apps/web/src/app/billing/organizations/subscription-status.component.html @@ -14,7 +14,7 @@
{{ "billingPlan" | i18n }}
{{ planName }}
- +
{{ data.status.label }}
diff --git a/apps/web/src/app/billing/organizations/subscription-status.component.ts b/apps/web/src/app/billing/organizations/subscription-status.component.ts index 54af940be5..9a0b634edc 100644 --- a/apps/web/src/app/billing/organizations/subscription-status.component.ts +++ b/apps/web/src/app/billing/organizations/subscription-status.component.ts @@ -5,11 +5,11 @@ import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/mode import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; type ComponentData = { - status: { + status?: { label: string; value: string; }; - date: { + date?: { label: string; value: string; }; @@ -44,13 +44,15 @@ export class SubscriptionStatusComponent { } get status(): string { - return this.subscription.status != "canceled" && this.subscription.cancelAtEndDate - ? "pending_cancellation" - : this.subscription.status; + return this.subscription + ? this.subscription.status != "canceled" && this.subscription.cancelAtEndDate + ? "pending_cancellation" + : this.subscription.status + : "free"; } get subscription() { - return this.organizationSubscriptionResponse.subscription; + return this.organizationSubscriptionResponse?.subscription; } get data(): ComponentData { @@ -61,6 +63,9 @@ export class SubscriptionStatusComponent { const cancellationDateLabel = this.i18nService.t("cancellationDate"); switch (this.status) { + case "free": { + return {}; + } case "trialing": { return { status: { diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog.component.html b/apps/web/src/app/billing/shared/adjust-payment-dialog.component.html new file mode 100644 index 0000000000..0f92b023b1 --- /dev/null +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog.component.html @@ -0,0 +1,25 @@ +
+ + + + + + + + + + +
diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts new file mode 100644 index 0000000000..41d0ad7e7a --- /dev/null +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts @@ -0,0 +1,110 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject, ViewChild } from "@angular/core"; +import { FormGroup } from "@angular/forms"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction"; +import { PaymentMethodType } from "@bitwarden/common/billing/enums"; +import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request"; +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 { DialogService } from "@bitwarden/components"; + +import { PaymentComponent } from "./payment.component"; +import { TaxInfoComponent } from "./tax-info.component"; + +export interface AdjustPaymentDialogData { + organizationId: string; + currentType: PaymentMethodType; +} + +export enum AdjustPaymentDialogResult { + Adjusted = "adjusted", + Cancelled = "cancelled", +} + +@Component({ + templateUrl: "adjust-payment-dialog.component.html", +}) +export class AdjustPaymentDialogComponent { + @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent; + @ViewChild(TaxInfoComponent, { static: true }) taxInfoComponent: TaxInfoComponent; + + organizationId: string; + currentType: PaymentMethodType; + paymentMethodType = PaymentMethodType; + + protected DialogResult = AdjustPaymentDialogResult; + protected formGroup = new FormGroup({}); + + constructor( + private dialogRef: DialogRef, + @Inject(DIALOG_DATA) protected data: AdjustPaymentDialogData, + private apiService: ApiService, + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private logService: LogService, + private organizationApiService: OrganizationApiServiceAbstraction, + private paymentMethodWarningService: PaymentMethodWarningService, + ) { + this.organizationId = data.organizationId; + this.currentType = data.currentType; + } + + submit = async () => { + const request = new PaymentRequest(); + const response = this.paymentComponent.createPaymentToken().then((result) => { + request.paymentToken = result[0]; + request.paymentMethodType = result[1]; + request.postalCode = this.taxInfoComponent.taxInfo.postalCode; + request.country = this.taxInfoComponent.taxInfo.country; + if (this.organizationId == null) { + return this.apiService.postAccountPayment(request); + } else { + request.taxId = this.taxInfoComponent.taxInfo.taxId; + request.state = this.taxInfoComponent.taxInfo.state; + request.line1 = this.taxInfoComponent.taxInfo.line1; + request.line2 = this.taxInfoComponent.taxInfo.line2; + request.city = this.taxInfoComponent.taxInfo.city; + request.state = this.taxInfoComponent.taxInfo.state; + return this.organizationApiService.updatePayment(this.organizationId, request); + } + }); + await response; + if (this.organizationId) { + await this.paymentMethodWarningService.removeSubscriptionRisk(this.organizationId); + } + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("updatedPaymentMethod"), + ); + this.dialogRef.close(AdjustPaymentDialogResult.Adjusted); + }; + + changeCountry() { + if (this.taxInfoComponent.taxInfo.country === "US") { + this.paymentComponent.hideBank = !this.organizationId; + } else { + this.paymentComponent.hideBank = true; + if (this.paymentComponent.method === PaymentMethodType.BankAccount) { + this.paymentComponent.method = PaymentMethodType.Card; + this.paymentComponent.changeMethod(); + } + } + } +} + +/** + * Strongly typed helper to open a AdjustPaymentDialog + * @param dialogService Instance of the dialog service that will be used to open the dialog + * @param config Configuration for the dialog + */ +export function openAdjustPaymentDialog( + dialogService: DialogService, + config: DialogConfig, +) { + return dialogService.open(AdjustPaymentDialogComponent, config); +} diff --git a/apps/web/src/app/billing/shared/adjust-payment.component.html b/apps/web/src/app/billing/shared/adjust-payment.component.html deleted file mode 100644 index 724e7a44c2..0000000000 --- a/apps/web/src/app/billing/shared/adjust-payment.component.html +++ /dev/null @@ -1,19 +0,0 @@ -
-
- -

- {{ (currentType != null ? "changePaymentMethod" : "addPaymentMethod") | i18n }} -

- - - - -
-
diff --git a/apps/web/src/app/billing/shared/adjust-payment.component.ts b/apps/web/src/app/billing/shared/adjust-payment.component.ts deleted file mode 100644 index 7452344141..0000000000 --- a/apps/web/src/app/billing/shared/adjust-payment.component.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Component, EventEmitter, Input, Output, ViewChild } from "@angular/core"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction"; -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request"; -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 { PaymentComponent } from "./payment.component"; -import { TaxInfoComponent } from "./tax-info.component"; - -@Component({ - selector: "app-adjust-payment", - templateUrl: "adjust-payment.component.html", -}) -export class AdjustPaymentComponent { - @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent; - @ViewChild(TaxInfoComponent, { static: true }) taxInfoComponent: TaxInfoComponent; - - @Input() currentType?: PaymentMethodType; - @Input() organizationId: string; - @Output() onAdjusted = new EventEmitter(); - @Output() onCanceled = new EventEmitter(); - - paymentMethodType = PaymentMethodType; - formPromise: Promise; - - constructor( - private apiService: ApiService, - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private logService: LogService, - private organizationApiService: OrganizationApiServiceAbstraction, - private paymentMethodWarningService: PaymentMethodWarningService, - ) {} - - async submit() { - try { - const request = new PaymentRequest(); - this.formPromise = this.paymentComponent.createPaymentToken().then((result) => { - request.paymentToken = result[0]; - request.paymentMethodType = result[1]; - request.postalCode = this.taxInfoComponent.taxInfo.postalCode; - request.country = this.taxInfoComponent.taxInfo.country; - if (this.organizationId == null) { - return this.apiService.postAccountPayment(request); - } else { - request.taxId = this.taxInfoComponent.taxInfo.taxId; - request.state = this.taxInfoComponent.taxInfo.state; - request.line1 = this.taxInfoComponent.taxInfo.line1; - request.line2 = this.taxInfoComponent.taxInfo.line2; - request.city = this.taxInfoComponent.taxInfo.city; - request.state = this.taxInfoComponent.taxInfo.state; - return this.organizationApiService.updatePayment(this.organizationId, request); - } - }); - await this.formPromise; - if (this.organizationId) { - await this.paymentMethodWarningService.removeSubscriptionRisk(this.organizationId); - } - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("updatedPaymentMethod"), - ); - this.onAdjusted.emit(); - } catch (e) { - this.logService.error(e); - } - } - - cancel() { - this.onCanceled.emit(); - } - - changeCountry() { - if (this.taxInfoComponent.taxInfo.country === "US") { - this.paymentComponent.hideBank = !this.organizationId; - } else { - this.paymentComponent.hideBank = true; - if (this.paymentComponent.method === PaymentMethodType.BankAccount) { - this.paymentComponent.method = PaymentMethodType.Card; - this.paymentComponent.changeMethod(); - } - } - } -} diff --git a/apps/web/src/app/billing/shared/adjust-storage.component.html b/apps/web/src/app/billing/shared/adjust-storage.component.html index aa6daca335..a597a3ae5e 100644 --- a/apps/web/src/app/billing/shared/adjust-storage.component.html +++ b/apps/web/src/app/billing/shared/adjust-storage.component.html @@ -1,43 +1,35 @@ -
-
- -

{{ (add ? "addStorage" : "removeStorage") | i18n }}

-
-
- - + + + +

{{ (add ? "storageAddNote" : "storageRemoveNote") | i18n }}

+
+ + {{ (add ? "gbStorageAdd" : "gbStorageRemove") | i18n }} + + + {{ "total" | i18n }}: + {{ formGroup.get("storageAdjustment").value || 0 }} GB × + {{ storageGbPrice | currency: "$" }} = {{ adjustedStorageTotal | currency: "$" }} /{{ + interval | i18n + }} + +
-
-
- {{ "total" | i18n }}: {{ storageAdjustment || 0 }} GB × - {{ storageGbPrice | currency: "$" }} = {{ adjustedStorageTotal | currency: "$" }} /{{ - interval | i18n - }} -
- - - - {{ (add ? "storageAddNote" : "storageRemoveNote") | i18n }} - -
+ + + + + + diff --git a/apps/web/src/app/billing/shared/adjust-storage.component.ts b/apps/web/src/app/billing/shared/adjust-storage.component.ts index 25462c2829..fcdbc3437d 100644 --- a/apps/web/src/app/billing/shared/adjust-storage.component.ts +++ b/apps/web/src/app/billing/shared/adjust-storage.component.ts @@ -1,4 +1,6 @@ -import { Component, EventEmitter, Input, Output, ViewChild } from "@angular/core"; +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject, ViewChild } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -8,27 +10,45 @@ import { StorageRequest } from "@bitwarden/common/models/request/storage.request 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 { DialogService } from "@bitwarden/components"; import { PaymentComponent } from "./payment.component"; +export interface AdjustStorageDialogData { + storageGbPrice: number; + add: boolean; + organizationId?: string; + interval?: string; +} + +export enum AdjustStorageDialogResult { + Adjusted = "adjusted", + Cancelled = "cancelled", +} + @Component({ - selector: "app-adjust-storage", templateUrl: "adjust-storage.component.html", }) export class AdjustStorageComponent { - @Input() storageGbPrice = 0; - @Input() add = true; - @Input() organizationId: string; - @Input() interval = "year"; - @Output() onAdjusted = new EventEmitter(); - @Output() onCanceled = new EventEmitter(); + storageGbPrice: number; + add: boolean; + organizationId: string; + interval: string; @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent; - storageAdjustment = 0; - formPromise: Promise; + protected DialogResult = AdjustStorageDialogResult; + protected formGroup = new FormGroup({ + storageAdjustment: new FormControl(0, [ + Validators.required, + Validators.min(0), + Validators.max(99), + ]), + }); constructor( + private dialogRef: DialogRef, + @Inject(DIALOG_DATA) protected data: AdjustStorageDialogData, private apiService: ApiService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, @@ -36,69 +56,74 @@ export class AdjustStorageComponent { private activatedRoute: ActivatedRoute, private logService: LogService, private organizationApiService: OrganizationApiServiceAbstraction, - ) {} + ) { + this.storageGbPrice = data.storageGbPrice; + this.add = data.add; + this.organizationId = data.organizationId; + this.interval = data.interval || "year"; + } - async submit() { - try { - const request = new StorageRequest(); - request.storageGbAdjustment = this.storageAdjustment; - if (!this.add) { - request.storageGbAdjustment *= -1; - } - - let paymentFailed = false; - const action = async () => { - let response: Promise; - if (this.organizationId == null) { - response = this.formPromise = this.apiService.postAccountStorage(request); - } else { - response = this.formPromise = this.organizationApiService.updateStorage( - this.organizationId, - request, - ); - } - const result = await response; - if (result != null && result.paymentIntentClientSecret != null) { - try { - await this.paymentComponent.handleStripeCardPayment( - result.paymentIntentClientSecret, - null, - ); - } catch { - paymentFailed = true; - } - } - }; - this.formPromise = action(); - await this.formPromise; - this.onAdjusted.emit(this.storageAdjustment); - if (paymentFailed) { - this.platformUtilsService.showToast( - "warning", - null, - this.i18nService.t("couldNotChargeCardPayInvoice"), - { timeout: 10000 }, - ); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["../billing"], { relativeTo: this.activatedRoute }); - } else { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), - ); - } - } catch (e) { - this.logService.error(e); + submit = async () => { + const request = new StorageRequest(); + request.storageGbAdjustment = this.formGroup.value.storageAdjustment; + if (!this.add) { + request.storageGbAdjustment *= -1; } - } - cancel() { - this.onCanceled.emit(); - } + let paymentFailed = false; + const action = async () => { + let response: Promise; + if (this.organizationId == null) { + response = this.apiService.postAccountStorage(request); + } else { + response = this.organizationApiService.updateStorage(this.organizationId, request); + } + const result = await response; + if (result != null && result.paymentIntentClientSecret != null) { + try { + await this.paymentComponent.handleStripeCardPayment( + result.paymentIntentClientSecret, + null, + ); + } catch { + paymentFailed = true; + } + } + }; + await action(); + this.dialogRef.close(AdjustStorageDialogResult.Adjusted); + if (paymentFailed) { + this.platformUtilsService.showToast( + "warning", + null, + this.i18nService.t("couldNotChargeCardPayInvoice"), + { timeout: 10000 }, + ); + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router.navigate(["../billing"], { relativeTo: this.activatedRoute }); + } else { + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), + ); + } + }; get adjustedStorageTotal(): number { - return this.storageGbPrice * this.storageAdjustment; + return this.storageGbPrice * this.formGroup.value.storageAdjustment; } } + +/** + * Strongly typed helper to open an AdjustStorageDialog + * @param dialogService Instance of the dialog service that will be used to open the dialog + * @param config Configuration for the dialog + */ +export function openAdjustStorageDialog( + dialogService: DialogService, + config: DialogConfig, +) { + return dialogService.open(AdjustStorageComponent, config); +} diff --git a/apps/web/src/app/billing/shared/billing-history.component.html b/apps/web/src/app/billing/shared/billing-history.component.html index 56a8a990d4..1719a59076 100644 --- a/apps/web/src/app/billing/shared/billing-history.component.html +++ b/apps/web/src/app/billing/shared/billing-history.component.html @@ -1,65 +1,72 @@ -

{{ "invoices" | i18n }}

-

{{ "noInvoices" | i18n }}

- - - - - + + + + + + + + + +

{{ "transactions" | i18n }}

+

+ {{ "noTransactions" | i18n }} +

+ + +
+ + + + - - - - -
{{ i.date | date: "mediumDate" }} - +

{{ "invoices" | i18n }}

+

{{ "noInvoices" | i18n }}

+ + +
{{ i.date | date: "mediumDate" }} + + + + {{ "invoiceNumber" | i18n: i.number }} + {{ i.amount | currency: "$" }} + + + {{ "paid" | i18n }} + + + + {{ "unpaid" | i18n }} + +
{{ t.createdDate | date: "mediumDate" }} + + {{ "chargeNoun" | i18n }} + + {{ "refundNoun" | i18n }} + + + {{ t.details }} + - - - {{ "invoiceNumber" | i18n: i.number }} - {{ i.amount | currency: "$" }} - - - {{ "paid" | i18n }} - - - - {{ "unpaid" | i18n }} - -
-

{{ "transactions" | i18n }}

-

{{ "noTransactions" | i18n }}

- - - - - - - - - -
{{ t.createdDate | date: "mediumDate" }} - - {{ "chargeNoun" | i18n }} - - {{ "refundNoun" | i18n }} - - - {{ t.details }} - - {{ t.amount | currency: "$" }} -
-* {{ "chargesStatement" | i18n: "BITWARDEN" }} + {{ t.amount | currency: "$" }} + + + + + * {{ "chargesStatement" | i18n: "BITWARDEN" }} + 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 2f773870aa..65a651b73d 100644 --- a/apps/web/src/app/billing/shared/billing-shared.module.ts +++ b/apps/web/src/app/billing/shared/billing-shared.module.ts @@ -4,7 +4,7 @@ import { HeaderModule } from "../../layouts/header/header.module"; import { SharedModule } from "../../shared"; import { AddCreditComponent } from "./add-credit.component"; -import { AdjustPaymentComponent } from "./adjust-payment.component"; +import { AdjustPaymentDialogComponent } from "./adjust-payment-dialog.component"; import { AdjustStorageComponent } from "./adjust-storage.component"; import { BillingHistoryComponent } from "./billing-history.component"; import { OffboardingSurveyComponent } from "./offboarding-survey.component"; @@ -18,7 +18,7 @@ import { UpdateLicenseComponent } from "./update-license.component"; imports: [SharedModule, PaymentComponent, TaxInfoComponent, HeaderModule], declarations: [ AddCreditComponent, - AdjustPaymentComponent, + AdjustPaymentDialogComponent, AdjustStorageComponent, BillingHistoryComponent, PaymentMethodComponent, 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 cfe98178b0..5f78294fa6 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.html +++ b/apps/web/src/app/billing/shared/payment-method.component.html @@ -15,7 +15,7 @@
- +

{{ "paymentMethod" | i18n }}

@@ -102,23 +102,9 @@ {{ paymentSource.description }}

- - -

{{ "paymentChargedWithUnpaidSubscription" | i18n }}

{{ "taxInformation" | 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 d2b65968c3..fee97cb912 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.ts +++ b/apps/web/src/app/billing/shared/payment-method.component.ts @@ -1,6 +1,7 @@ import { Component, 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"; @@ -14,6 +15,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; +import { + AdjustPaymentDialogResult, + openAdjustPaymentDialog, +} from "./adjust-payment-dialog.component"; import { TaxInfoComponent } from "./tax-info.component"; @Component({ @@ -25,7 +30,6 @@ export class PaymentMethodComponent implements OnInit { loading = false; firstLoaded = false; - showAdjustPayment = false; showAddCredit = false; billing: BillingPaymentResponse; org: OrganizationSubscriptionResponse; @@ -120,18 +124,18 @@ export class PaymentMethodComponent implements OnInit { } } - changePayment() { - this.showAdjustPayment = true; - } - - closePayment(load: boolean) { - this.showAdjustPayment = false; - if (load) { - // 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.load(); + changePayment = async () => { + const dialogRef = openAdjustPaymentDialog(this.dialogService, { + data: { + organizationId: this.organizationId, + currentType: this.paymentSource !== null ? this.paymentSource.type : null, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === AdjustPaymentDialogResult.Adjusted) { + await this.load(); } - } + }; async verifyBank() { if (this.loading || !this.forOrganization) { diff --git a/apps/web/src/app/billing/shared/payment.component.ts b/apps/web/src/app/billing/shared/payment.component.ts index 6e34962cb7..652bed7801 100644 --- a/apps/web/src/app/billing/shared/payment.component.ts +++ b/apps/web/src/app/billing/shared/payment.component.ts @@ -287,7 +287,7 @@ export class PaymentComponent implements OnInit, OnDestroy { )})`; this.StripeElementStyle.invalid.color = `rgb(${style.getPropertyValue("--color-text-main")})`; this.StripeElementStyle.invalid.borderColor = `rgb(${style.getPropertyValue( - "--color-danger-500", + "--color-danger-600", )})`; }); } diff --git a/apps/web/src/app/billing/shared/sm-subscribe.component.html b/apps/web/src/app/billing/shared/sm-subscribe.component.html index c9243e29a6..6cdaeb9476 100644 --- a/apps/web/src/app/billing/shared/sm-subscribe.component.html +++ b/apps/web/src/app/billing/shared/sm-subscribe.component.html @@ -20,10 +20,10 @@
  • {{ "unlimitedProjects" | i18n }}
  • -
  • {{ "serviceAccountsIncluded" | i18n: serviceAccountsIncluded }}
  • +
  • {{ "machineAccountsIncluded" | i18n: serviceAccountsIncluded }}
  • {{ - "additionalServiceAccountCost" | i18n: (monthlyCostPerServiceAccount | currency: "$") + "additionalMachineAccountCost" | i18n: (monthlyCostPerServiceAccount | currency: "$") }}
  • @@ -54,12 +54,12 @@
    - {{ "additionalServiceAccounts" | i18n }} + {{ "additionalMachineAccounts" | i18n }} - {{ "includedServiceAccounts" | i18n: serviceAccountsIncluded }} + {{ "includedMachineAccounts" | i18n: serviceAccountsIncluded }} {{ - "addAdditionalServiceAccounts" | i18n: (monthlyCostPerServiceAccount | currency: "$") + "addAdditionalMachineAccounts" | i18n: (monthlyCostPerServiceAccount | currency: "$") }} diff --git a/apps/web/src/app/billing/shared/tax-info.component.html b/apps/web/src/app/billing/shared/tax-info.component.html index caf92f4189..30cca550d3 100644 --- a/apps/web/src/app/billing/shared/tax-info.component.html +++ b/apps/web/src/app/billing/shared/tax-info.component.html @@ -279,10 +279,7 @@ />
    -
    +
    -
    +
    - +
    -
    +
    diff --git a/apps/web/src/app/billing/shared/tax-info.component.ts b/apps/web/src/app/billing/shared/tax-info.component.ts index 2046cf44be..a704c86eb5 100644 --- a/apps/web/src/app/billing/shared/tax-info.component.ts +++ b/apps/web/src/app/billing/shared/tax-info.component.ts @@ -3,7 +3,7 @@ import { ActivatedRoute } from "@angular/router"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/organization-tax-info-update.request"; +import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; import { TaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/tax-info-update.request"; import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; import { TaxRateResponse } from "@bitwarden/common/billing/models/response/tax-rate.response"; @@ -29,6 +29,7 @@ export class TaxInfoComponent { loading = true; organizationId: string; + providerId: string; taxInfo: TaxInfoView = { taxId: null, line1: null, @@ -61,6 +62,12 @@ export class TaxInfoComponent { ) {} async ngOnInit() { + // Provider setup + // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe + this.route.queryParams.subscribe((params) => { + this.providerId = params.providerId; + }); + // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { this.organizationId = params.organizationId; @@ -126,9 +133,25 @@ export class TaxInfoComponent { } } + get showTaxIdCheckbox() { + return ( + (this.organizationId || this.providerId) && + this.taxInfo.country !== "US" && + this.countrySupportsTax(this.taxInfo.country) + ); + } + + get showTaxIdFields() { + return ( + (this.organizationId || this.providerId) && + this.taxInfo.includeTaxId && + this.countrySupportsTax(this.taxInfo.country) + ); + } + getTaxInfoRequest(): TaxInfoUpdateRequest { - if (this.organizationId) { - const request = new OrganizationTaxInfoUpdateRequest(); + if (this.organizationId || this.providerId) { + const request = new ExpandedTaxInfoUpdateRequest(); request.country = this.taxInfo.country; request.postalCode = this.taxInfo.postalCode; @@ -164,7 +187,7 @@ export class TaxInfoComponent { return this.organizationId ? this.organizationApiService.updateTaxInfo( this.organizationId, - request as OrganizationTaxInfoUpdateRequest, + request as ExpandedTaxInfoUpdateRequest, ) : this.apiService.putTaxInfo(request); } diff --git a/apps/web/src/app/components/selectable-avatar.component.ts b/apps/web/src/app/components/selectable-avatar.component.ts index 4a138ec989..1de722461a 100644 --- a/apps/web/src/app/components/selectable-avatar.component.ts +++ b/apps/web/src/app/components/selectable-avatar.component.ts @@ -41,13 +41,13 @@ export class SelectableAvatarComponent { .concat(["tw-cursor-pointer", "tw-outline", "tw-outline-solid", "tw-outline-offset-1"]) .concat( this.selected - ? ["tw-outline-[3px]", "tw-outline-primary-500"] + ? ["tw-outline-[3px]", "tw-outline-primary-600"] : [ "tw-outline-0", "hover:tw-outline-1", "hover:tw-outline-primary-300", "focus:tw-outline-2", - "focus:tw-outline-primary-500", + "focus:tw-outline-primary-600", ], ); } diff --git a/apps/web/src/app/core/broadcaster-messaging.service.ts b/apps/web/src/app/core/broadcaster-messaging.service.ts deleted file mode 100644 index 7c8e4eef43..0000000000 --- a/apps/web/src/app/core/broadcaster-messaging.service.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Injectable } from "@angular/core"; - -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -@Injectable() -export class BroadcasterMessagingService implements MessagingService { - constructor(private broadcasterService: BroadcasterService) {} - - send(subscriber: string, arg: any = {}) { - const message = Object.assign({}, { command: subscriber }, arg); - this.broadcasterService.send(message); - } -} diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index bd514b1d18..a274764756 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -1,6 +1,7 @@ import { CommonModule } from "@angular/common"; import { APP_INITIALIZER, NgModule, Optional, SkipSelf } from "@angular/core"; +import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { SECURE_STORAGE, STATE_FACTORY, @@ -12,6 +13,7 @@ import { OBSERVABLE_DISK_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE, WINDOW, + SafeInjectionToken, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service"; @@ -20,7 +22,6 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; @@ -49,7 +50,6 @@ import { WebStorageServiceProvider } from "../platform/web-storage-service.provi import { WindowStorageService } from "../platform/window-storage.service"; import { CollectionAdminService } from "../vault/core/collection-admin.service"; -import { BroadcasterMessagingService } from "./broadcaster-messaging.service"; import { EventService } from "./event.service"; import { InitService } from "./init.service"; import { ModalService } from "./modal.service"; @@ -58,97 +58,117 @@ import { Account, GlobalState, StateService } from "./state"; import { WebFileDownloadService } from "./web-file-download.service"; import { WebPlatformUtilsService } from "./web-platform-utils.service"; +/** + * Provider definitions used in the ngModule. + * Add your provider definition here using the safeProvider function as a wrapper. This will give you type safety. + * If you need help please ask for it, do NOT change the type of this array. + */ +const safeProviders: SafeProvider[] = [ + safeProvider(InitService), + safeProvider(RouterService), + safeProvider(EventService), + safeProvider(PolicyListService), + safeProvider({ + provide: APP_INITIALIZER as SafeInjectionToken<() => void>, + useFactory: (initService: InitService) => initService.init(), + deps: [InitService], + multi: true, + }), + safeProvider({ + provide: STATE_FACTORY, + useValue: new StateFactory(GlobalState, Account), + }), + safeProvider({ + provide: STATE_SERVICE_USE_CACHE, + useValue: false, + }), + safeProvider({ + provide: I18nServiceAbstraction, + useClass: I18nService, + deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY, GlobalStateProvider], + }), + safeProvider({ provide: AbstractStorageService, useClass: HtmlStorageService, deps: [] }), + safeProvider({ + provide: SECURE_STORAGE, + // TODO: platformUtilsService.isDev has a helper for this, but using that service here results in a circular dependency. + // We have a tech debt item in the backlog to break up platformUtilsService, but in the meantime simply checking the environment here is less cumbersome. + useClass: process.env.NODE_ENV === "development" ? HtmlStorageService : MemoryStorageService, + deps: [], + }), + safeProvider({ + provide: MEMORY_STORAGE, + useClass: MemoryStorageService, + deps: [], + }), + safeProvider({ + provide: OBSERVABLE_MEMORY_STORAGE, + useClass: MemoryStorageServiceForStateProviders, + deps: [], + }), + safeProvider({ + provide: OBSERVABLE_DISK_STORAGE, + useFactory: () => new WindowStorageService(window.sessionStorage), + deps: [], + }), + safeProvider({ + provide: PlatformUtilsServiceAbstraction, + useClass: WebPlatformUtilsService, + useAngularDecorators: true, + }), + safeProvider({ + provide: ModalServiceAbstraction, + useClass: ModalService, + useAngularDecorators: true, + }), + safeProvider(StateService), + safeProvider({ + provide: BaseStateServiceAbstraction, + useExisting: StateService, + }), + safeProvider({ + provide: FileDownloadService, + useClass: WebFileDownloadService, + useAngularDecorators: true, + }), + safeProvider(CollectionAdminService), + safeProvider({ + provide: WindowStorageService, + useFactory: () => new WindowStorageService(window.localStorage), + deps: [], + }), + safeProvider({ + provide: OBSERVABLE_DISK_LOCAL_STORAGE, + useExisting: WindowStorageService, + }), + safeProvider({ + provide: StorageServiceProvider, + useClass: WebStorageServiceProvider, + deps: [OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE], + }), + safeProvider({ + provide: MigrationRunner, + useClass: WebMigrationRunner, + deps: [AbstractStorageService, LogService, MigrationBuilderService, WindowStorageService], + }), + safeProvider({ + provide: EnvironmentService, + useClass: WebEnvironmentService, + deps: [WINDOW, StateProvider, AccountService], + }), + safeProvider({ + provide: ThemeStateService, + useFactory: (globalStateProvider: GlobalStateProvider) => + // Web chooses to have Light as the default theme + new DefaultThemeStateService(globalStateProvider, ThemeType.Light), + deps: [GlobalStateProvider], + }), +]; + @NgModule({ declarations: [], imports: [CommonModule, JslibServicesModule], - providers: [ - InitService, - RouterService, - EventService, - PolicyListService, - { - provide: APP_INITIALIZER, - useFactory: (initService: InitService) => initService.init(), - deps: [InitService], - multi: true, - }, - { - provide: STATE_FACTORY, - useValue: new StateFactory(GlobalState, Account), - }, - { - provide: STATE_SERVICE_USE_CACHE, - useValue: false, - }, - { - provide: I18nServiceAbstraction, - useClass: I18nService, - deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY, GlobalStateProvider], - }, - { provide: AbstractStorageService, useClass: HtmlStorageService }, - { - provide: SECURE_STORAGE, - // TODO: platformUtilsService.isDev has a helper for this, but using that service here results in a circular dependency. - // We have a tech debt item in the backlog to break up platformUtilsService, but in the meantime simply checking the environment here is less cumbersome. - useClass: process.env.NODE_ENV === "development" ? HtmlStorageService : MemoryStorageService, - }, - { - provide: MEMORY_STORAGE, - useClass: MemoryStorageService, - }, - { provide: OBSERVABLE_MEMORY_STORAGE, useClass: MemoryStorageServiceForStateProviders }, - { - provide: OBSERVABLE_DISK_STORAGE, - useFactory: () => new WindowStorageService(window.sessionStorage), - }, - { - provide: PlatformUtilsServiceAbstraction, - useClass: WebPlatformUtilsService, - }, - { provide: MessagingServiceAbstraction, useClass: BroadcasterMessagingService }, - { provide: ModalServiceAbstraction, useClass: ModalService }, - StateService, - { - provide: BaseStateServiceAbstraction, - useExisting: StateService, - }, - { - provide: FileDownloadService, - useClass: WebFileDownloadService, - }, - CollectionAdminService, - { - provide: OBSERVABLE_DISK_LOCAL_STORAGE, - useFactory: () => new WindowStorageService(window.localStorage), - }, - { - provide: StorageServiceProvider, - useClass: WebStorageServiceProvider, - deps: [OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE], - }, - { - provide: MigrationRunner, - useClass: WebMigrationRunner, - deps: [ - AbstractStorageService, - LogService, - MigrationBuilderService, - OBSERVABLE_DISK_LOCAL_STORAGE, - ], - }, - { - provide: EnvironmentService, - useClass: WebEnvironmentService, - deps: [WINDOW, StateProvider, AccountService], - }, - { - provide: ThemeStateService, - useFactory: (globalStateProvider: GlobalStateProvider) => - // Web chooses to have Light as the default theme - new DefaultThemeStateService(globalStateProvider, ThemeType.Light), - deps: [GlobalStateProvider], - }, - ], + // Do not register your dependency here! Add it to the typesafeProviders array using the helper function + providers: safeProviders, }) export class CoreModule { constructor(@Optional() @SkipSelf() parentModule?: CoreModule) { diff --git a/apps/web/src/app/core/init.service.ts b/apps/web/src/app/core/init.service.ts index d5576d3bf7..dab6ed5e3d 100644 --- a/apps/web/src/app/core/init.service.ts +++ b/apps/web/src/app/core/init.service.ts @@ -11,6 +11,7 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt. import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; @@ -27,12 +28,14 @@ export class InitService { private cryptoService: CryptoServiceAbstraction, private themingService: AbstractThemingService, private encryptService: EncryptService, + private userKeyInitService: UserKeyInitService, @Inject(DOCUMENT) private document: Document, ) {} init() { return async () => { await this.stateService.init(); + this.userKeyInitService.listenForActiveUserChangesToSetUserKey(); setTimeout(() => this.notificationsService.init(), 3000); await this.vaultTimeoutService.init(true); diff --git a/apps/web/src/app/core/state/state.service.ts b/apps/web/src/app/core/state/state.service.ts index 54e456d34c..1ae62d8591 100644 --- a/apps/web/src/app/core/state/state.service.ts +++ b/apps/web/src/app/core/state/state.service.ts @@ -18,7 +18,6 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service"; -import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { Account } from "./account"; import { GlobalState } from "./global-state"; @@ -57,19 +56,6 @@ export class StateService extends BaseStateService { await super.addAccount(account); } - async getEncryptedCiphers(options?: StorageOptions): Promise<{ [id: string]: CipherData }> { - options = this.reconcileOptions(options, await this.defaultInMemoryOptions()); - return await super.getEncryptedCiphers(options); - } - - async setEncryptedCiphers( - value: { [id: string]: CipherData }, - options?: StorageOptions, - ): Promise { - options = this.reconcileOptions(options, await this.defaultInMemoryOptions()); - return await super.setEncryptedCiphers(value, options); - } - override async getLastSync(options?: StorageOptions): Promise { options = this.reconcileOptions(options, await this.defaultInMemoryOptions()); return await super.getLastSync(options); diff --git a/apps/web/src/app/layouts/header/web-header.component.html b/apps/web/src/app/layouts/header/web-header.component.html index 514e5deebd..e24013de6f 100644 --- a/apps/web/src/app/layouts/header/web-header.component.html +++ b/apps/web/src/app/layouts/header/web-header.component.html @@ -1,16 +1,29 @@ - {{ "newWebApp" | i18n }} + {{ unassignedItemsBannerService.bannerText$ | async | i18n }} + {{ "unassignedItemsBannerCTAPartOne" | i18n }} {{ "adminConsole" | i18n }} + {{ "unassignedItemsBannerCTAPartTwo" | i18n }} + {{ "releaseBlog" | i18n }}{{ "learnMore" | i18n }}
    ; protected selfHosted: boolean; protected hostname = location.hostname; + protected unassignedItemsBannerEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.UnassignedItemsBanner, + ); constructor( private route: ActivatedRoute, @@ -38,7 +42,8 @@ export class WebHeaderComponent { private platformUtilsService: PlatformUtilsService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private messagingService: MessagingService, - protected webLayoutMigrationBannerService: WebLayoutMigrationBannerService, + protected unassignedItemsBannerService: UnassignedItemsBannerService, + private configService: ConfigService, ) { this.routeData$ = this.route.data.pipe( map((params) => { diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html index 9068f9c071..f038fafecc 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html +++ b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html @@ -14,10 +14,10 @@ [routerLink]="product.appRoute" [ngClass]=" product.isActive - ? 'tw-bg-primary-500 tw-font-bold !tw-text-contrast tw-ring-offset-2 hover:tw-bg-primary-500' + ? 'tw-bg-primary-600 tw-font-bold !tw-text-contrast tw-ring-offset-2 hover:tw-bg-primary-600' : '' " - class="tw-group tw-flex tw-h-24 tw-w-28 tw-flex-col tw-items-center tw-justify-center tw-rounded tw-p-1 tw-text-primary-500 tw-outline-none hover:tw-bg-background-alt hover:tw-text-primary-700 hover:tw-no-underline focus-visible:!tw-ring-2 focus-visible:!tw-ring-primary-700" + class="tw-group tw-flex tw-h-24 tw-w-28 tw-flex-col tw-items-center tw-justify-center tw-rounded tw-p-1 tw-text-primary-600 tw-outline-none hover:tw-bg-background-alt hover:tw-text-primary-700 hover:tw-no-underline focus-visible:!tw-ring-2 focus-visible:!tw-ring-primary-700" ariaCurrentWhenActive="page" > diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.component.html b/apps/web/src/app/layouts/product-switcher/product-switcher.component.html index 449557b6f4..c9956f05e4 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.component.html +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.component.html @@ -1,10 +1,8 @@ - - - - + + diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts b/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts index a461785c31..eff5f08702 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts @@ -1,16 +1,11 @@ import { AfterViewInit, ChangeDetectorRef, Component, Input } from "@angular/core"; import { IconButtonType } from "@bitwarden/components/src/icon-button/icon-button.component"; - -import { flagEnabled } from "../../../utils/flags"; - @Component({ selector: "product-switcher", templateUrl: "./product-switcher.component.html", }) export class ProductSwitcherComponent implements AfterViewInit { - protected isEnabled = flagEnabled("secretsManager"); - /** * Passed to the product switcher's `bitIconButton` */ diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index e5c2f353c0..066ed5db10 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -13,6 +13,7 @@ import { flagEnabled, Flags } from "../utils/flags"; import { AcceptFamilySponsorshipComponent } from "./admin-console/organizations/sponsorships/accept-family-sponsorship.component"; import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizations/sponsorships/families-for-enterprise-setup.component"; +import { VerifyRecoverDeleteProviderComponent } from "./admin-console/providers/verify-recover-delete-provider.component"; import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component"; import { SponsoredFamiliesComponent } from "./admin-console/settings/sponsored-families.component"; import { AcceptOrganizationComponent } from "./auth/accept-organization.component"; @@ -156,6 +157,12 @@ const routes: Routes = [ canActivate: [UnauthGuard], data: { titleId: "deleteAccount" }, }, + { + path: "verify-recover-delete-provider", + component: VerifyRecoverDeleteProviderComponent, + canActivate: [UnauthGuard], + data: { titleId: "deleteAccount" }, + }, { path: "send/:sendId/:key", component: AccessComponent, diff --git a/apps/web/src/app/oss.module.ts b/apps/web/src/app/oss.module.ts index 73c03fd5dc..3f18440d23 100644 --- a/apps/web/src/app/oss.module.ts +++ b/apps/web/src/app/oss.module.ts @@ -1,6 +1,5 @@ import { NgModule } from "@angular/core"; -import { OrganizationUserModule } from "./admin-console/organizations/users/organization-user.module"; import { AuthModule } from "./auth"; import { LoginModule } from "./auth/login/login.module"; import { TrialInitiationModule } from "./auth/trial-initiation/trial-initiation.module"; @@ -16,7 +15,6 @@ import { VaultFilterModule } from "./vault/individual-vault/vault-filter/vault-f TrialInitiationModule, VaultFilterModule, OrganizationBadgeModule, - OrganizationUserModule, LoginModule, AuthModule, AccessComponent, diff --git a/apps/web/src/app/settings/low-kdf.component.html b/apps/web/src/app/settings/low-kdf.component.html index e140f345e9..fd191b21e8 100644 --- a/apps/web/src/app/settings/low-kdf.component.html +++ b/apps/web/src/app/settings/low-kdf.component.html @@ -1,5 +1,5 @@ -
    -
    +
    +
    {{ "lowKdfIterations" | i18n }}
    diff --git a/apps/web/src/app/shared/components/onboarding/onboarding.component.html b/apps/web/src/app/shared/components/onboarding/onboarding.component.html index 6a0bacbf89..ecf1eb75dd 100644 --- a/apps/web/src/app/shared/components/onboarding/onboarding.component.html +++ b/apps/web/src/app/shared/components/onboarding/onboarding.component.html @@ -1,7 +1,7 @@
    - +
    {{ title }}
    diff --git a/apps/web/src/app/shared/components/onboarding/onboarding.stories.ts b/apps/web/src/app/shared/components/onboarding/onboarding.stories.ts index c1529dc81e..4088b7335c 100644 --- a/apps/web/src/app/shared/components/onboarding/onboarding.stories.ts +++ b/apps/web/src/app/shared/components/onboarding/onboarding.stories.ts @@ -39,7 +39,7 @@ const Template: Story = (args) => ({ template: ` diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 586f207962..8f6a1eaedc 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -13,6 +13,7 @@ import { ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent } f import { UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent } from "../admin-console/organizations/tools/unsecured-websites-report.component"; import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from "../admin-console/organizations/tools/weak-passwords-report.component"; import { ProvidersComponent } from "../admin-console/providers/providers.component"; +import { VerifyRecoverDeleteProviderComponent } from "../admin-console/providers/verify-recover-delete-provider.component"; import { SponsoredFamiliesComponent } from "../admin-console/settings/sponsored-families.component"; import { SponsoringOrgRowComponent } from "../admin-console/settings/sponsoring-org-row.component"; import { AcceptOrganizationComponent } from "../auth/accept-organization.component"; @@ -184,6 +185,7 @@ import { SharedModule } from "./shared.module"; VerifyEmailComponent, VerifyEmailTokenComponent, VerifyRecoverDeleteComponent, + VerifyRecoverDeleteProviderComponent, LowKdfComponent, ], exports: [ @@ -261,6 +263,7 @@ import { SharedModule } from "./shared.module"; VerifyEmailComponent, VerifyEmailTokenComponent, VerifyRecoverDeleteComponent, + VerifyRecoverDeleteProviderComponent, LowKdfComponent, HeaderModule, DangerZoneComponent, diff --git a/apps/web/src/app/shared/shared.module.ts b/apps/web/src/app/shared/shared.module.ts index bc775f07e2..1b04583a39 100644 --- a/apps/web/src/app/shared/shared.module.ts +++ b/apps/web/src/app/shared/shared.module.ts @@ -4,7 +4,6 @@ import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { RouterModule } from "@angular/router"; import { InfiniteScrollModule } from "ngx-infinite-scroll"; -import { ToastrModule } from "ngx-toastr"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { @@ -52,7 +51,6 @@ import "./locales"; ReactiveFormsModule, InfiniteScrollModule, RouterModule, - ToastrModule, JslibModule, // Component library modules @@ -90,7 +88,6 @@ import "./locales"; ReactiveFormsModule, InfiniteScrollModule, RouterModule, - ToastrModule, JslibModule, // Component library diff --git a/apps/web/src/app/tools/send/icons/expired-send.icon.ts b/apps/web/src/app/tools/send/icons/expired-send.icon.ts index b39cdca797..3ce0856a26 100644 --- a/apps/web/src/app/tools/send/icons/expired-send.icon.ts +++ b/apps/web/src/app/tools/send/icons/expired-send.icon.ts @@ -2,10 +2,10 @@ import { svgIcon } from "@bitwarden/components"; export const ExpiredSend = svgIcon` - - - - - + + + + + `; diff --git a/apps/web/src/app/tools/send/icons/no-send.icon.ts b/apps/web/src/app/tools/send/icons/no-send.icon.ts index 7811a4723b..f5494a4b3c 100644 --- a/apps/web/src/app/tools/send/icons/no-send.icon.ts +++ b/apps/web/src/app/tools/send/icons/no-send.icon.ts @@ -2,12 +2,12 @@ import { svgIcon } from "@bitwarden/components"; export const NoSend = svgIcon` - - - - - - - + + + + + + + `; diff --git a/apps/web/src/app/tools/send/send.component.ts b/apps/web/src/app/tools/send/send.component.ts index b5edd1433e..755435882a 100644 --- a/apps/web/src/app/tools/send/send.component.ts +++ b/apps/web/src/app/tools/send/send.component.ts @@ -92,6 +92,7 @@ export class SendComponent extends BaseSendComponent { } ngOnDestroy() { + this.dialogService.closeAll(); this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); } diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html index d0ffb90911..b64ce5bb00 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html @@ -65,17 +65,22 @@
    - {{ "grantCollectionAccess" | i18n }} - {{ - "grantCollectionAccessMembersOnly" | i18n - }} - {{ " " + ("adminCollectionAccess" | i18n) }} + + {{ "readOnlyCollectionAccess" | i18n }} + + + {{ "grantCollectionAccess" | i18n }} + {{ + "grantCollectionAccessMembersOnly" | i18n + }} + {{ " " + ("adminCollectionAccess" | i18n) }} +
    - -
    - - +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    + +
    +
    + +
    + + + + + diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts index 079e68fe21..83038d1bfc 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts @@ -1,13 +1,18 @@ import { Component } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; +import { UserVerificationDialogComponent } from "@bitwarden/auth/angular"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderUpdateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-update.request"; import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response"; +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 { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { DialogService } from "@bitwarden/components"; @Component({ selector: "provider-account", @@ -23,6 +28,11 @@ export class AccountComponent { private providerId: string; + protected enableDeleteProvider$ = this.configService.getFeatureFlag$( + FeatureFlag.EnableDeleteProvider, + false, + ); + constructor( private apiService: ApiService, private i18nService: I18nService, @@ -30,6 +40,9 @@ export class AccountComponent { private syncService: SyncService, private platformUtilsService: PlatformUtilsService, private logService: LogService, + private dialogService: DialogService, + private configService: ConfigService, + private providerApiService: ProviderApiServiceAbstraction, ) {} async ngOnInit() { @@ -38,7 +51,7 @@ export class AccountComponent { this.route.parent.parent.params.subscribe(async (params) => { this.providerId = params.providerId; try { - this.provider = await this.apiService.getProvider(this.providerId); + this.provider = await this.providerApiService.getProvider(this.providerId); } catch (e) { this.logService.error(`Handled exception: ${e}`); } @@ -53,7 +66,7 @@ export class AccountComponent { request.businessName = this.provider.businessName; request.billingEmail = this.provider.billingEmail; - this.formPromise = this.apiService.putProvider(this.providerId, request).then(() => { + this.formPromise = this.providerApiService.putProvider(this.providerId, request).then(() => { return this.syncService.fullSync(true); }); await this.formPromise; @@ -62,4 +75,60 @@ export class AccountComponent { this.logService.error(`Handled exception: ${e}`); } } + + async deleteProvider() { + const providerClients = await this.apiService.getProviderClients(this.providerId); + if (providerClients.data != null && providerClients.data.length > 0) { + await this.dialogService.openSimpleDialog({ + title: { key: "deleteProviderName", placeholders: [this.provider.name] }, + content: { key: "deleteProviderWarningDesc", placeholders: [this.provider.name] }, + acceptButtonText: { key: "ok" }, + type: "danger", + }); + + return false; + } + + const userVerified = await this.verifyUser(); + if (!userVerified) { + return; + } + + this.formPromise = this.providerApiService.deleteProvider(this.providerId); + try { + await this.formPromise; + this.platformUtilsService.showToast( + "success", + this.i18nService.t("providerDeleted"), + this.i18nService.t("providerDeletedDesc"), + ); + } catch (e) { + this.logService.error(e); + } + this.formPromise = null; + } + + private async verifyUser(): Promise { + const confirmDescription = "deleteProviderConfirmation"; + const result = await UserVerificationDialogComponent.open(this.dialogService, { + title: "deleteProvider", + bodyText: confirmDescription, + confirmButtonOptions: { + text: "deleteProvider", + type: "danger", + }, + }); + + // Handle the result of the dialog based on user action and verification success + if (result.userAction === "cancel") { + // User cancelled the dialog + return false; + } + + // User confirmed the dialog so check verification success + if (!result.verificationSuccess) { + return false; + } + return true; + } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/settings.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/settings.component.ts deleted file mode 100644 index 2418dbed41..0000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/settings.component.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Component } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; - -import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; - -@Component({ - selector: "provider-settings", - templateUrl: "settings.component.html", -}) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class SettingsComponent { - constructor( - private route: ActivatedRoute, - private providerService: ProviderService, - ) {} - - ngOnInit() { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.parent.params.subscribe(async (params) => { - await this.providerService.get(params.providerId); - }); - } -} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html index 1e7146bb58..d1cf666874 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html @@ -25,6 +25,9 @@ required />
    +
    + +
    diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts index b3d3112bf5..cf9af4f68a 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts @@ -1,9 +1,11 @@ -import { Component, OnInit } from "@angular/core"; +import { Component, OnInit, ViewChild } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { first } from "rxjs/operators"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request"; +import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -12,6 +14,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ProviderKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { TaxInfoComponent } from "@bitwarden/web-vault/app/billing"; @Component({ selector: "provider-setup", @@ -19,6 +22,8 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil export class SetupComponent implements OnInit { + @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent; + loading = true; authed = false; email: string; @@ -34,16 +39,21 @@ export class SetupComponent implements OnInit { false, ); + protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$( + FeatureFlag.EnableConsolidatedBilling, + false, + ); + constructor( private router: Router, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private route: ActivatedRoute, private cryptoService: CryptoService, - private apiService: ApiService, private syncService: SyncService, private validationService: ValidationService, private configService: ConfigService, + private providerApiService: ProviderApiServiceAbstraction, ) {} ngOnInit() { @@ -70,7 +80,7 @@ export class SetupComponent implements OnInit { // Check if provider exists, redirect if it does try { - const provider = await this.apiService.getProvider(this.providerId); + const provider = await this.providerApiService.getProvider(this.providerId); if (provider.name != null) { // 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 @@ -102,7 +112,23 @@ export class SetupComponent implements OnInit { request.token = this.token; request.key = key; - const provider = await this.apiService.postProviderSetup(this.providerId, request); + const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$); + + if (enableConsolidatedBilling) { + request.taxInfo = new ExpandedTaxInfoUpdateRequest(); + const taxInfoView = this.taxInfoComponent.taxInfo; + request.taxInfo.country = taxInfoView.country; + request.taxInfo.postalCode = taxInfoView.postalCode; + if (taxInfoView.includeTaxId) { + request.taxInfo.taxId = taxInfoView.taxId; + request.taxInfo.line1 = taxInfoView.line1; + request.taxInfo.line2 = taxInfoView.line2; + request.taxInfo.city = taxInfoView.city; + request.taxInfo.state = taxInfoView.state; + } + } + + const provider = await this.providerApiService.postProviderSetup(this.providerId, request); this.platformUtilsService.showToast("success", null, this.i18nService.t("providerSetup")); await this.syncService.fullSync(true); diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-organization.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-organization.component.html new file mode 100644 index 0000000000..4c5d9fca9b --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-organization.component.html @@ -0,0 +1,69 @@ +
    + + + {{ "newClientOrganization" | i18n }} + +
    +

    {{ "createNewClientToManageAsProvider" | i18n }}

    +
    + {{ "selectAPlan" | i18n }} + {{ "thirtyFivePercentDiscount" | i18n }} +
    + +
    +
    +
    +
    + {{ "selected" | i18n }} +
    +
    +

    {{ planCard.name }}

    + {{ + planCard.cost | currency: "$" + }} + /{{ "monthPerMember" | i18n }} +
    +
    +
    +
    +
    +
    + + + {{ "organizationName" | i18n }} + + + + + + {{ "clientOwnerEmail" | i18n }} + + + +
    +
    + + + {{ "seats" | i18n }} + + + +
    +
    + + + + +
    +
    diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-organization.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-organization.component.ts new file mode 100644 index 0000000000..8427572516 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-organization.component.ts @@ -0,0 +1,142 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject, OnInit } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; + +import { PlanType } from "@bitwarden/common/billing/enums"; +import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { DialogService } from "@bitwarden/components"; + +import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; + +type CreateClientOrganizationParams = { + providerId: string; + plans: PlanResponse[]; +}; + +export enum CreateClientOrganizationResultType { + Closed = "closed", + Submitted = "submitted", +} + +export const openCreateClientOrganizationDialog = ( + dialogService: DialogService, + dialogConfig: DialogConfig, +) => + dialogService.open( + CreateClientOrganizationComponent, + dialogConfig, + ); + +type PlanCard = { + name: string; + cost: number; + type: PlanType; + selected: boolean; +}; + +@Component({ + selector: "app-create-client-organization", + templateUrl: "./create-client-organization.component.html", +}) +export class CreateClientOrganizationComponent implements OnInit { + protected ResultType = CreateClientOrganizationResultType; + protected formGroup = this.formBuilder.group({ + clientOwnerEmail: ["", [Validators.required, Validators.email]], + organizationName: ["", Validators.required], + seats: [null, [Validators.required, Validators.min(1)]], + }); + protected planCards: PlanCard[]; + + constructor( + @Inject(DIALOG_DATA) private dialogParams: CreateClientOrganizationParams, + private dialogRef: DialogRef, + private formBuilder: FormBuilder, + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private webProviderService: WebProviderService, + ) {} + + protected getPlanCardContainerClasses(selected: boolean) { + switch (selected) { + case true: { + return [ + "tw-group", + "tw-cursor-pointer", + "tw-block", + "tw-rounded", + "tw-border", + "tw-border-solid", + "tw-border-primary-600", + "hover:tw-border-primary-700", + "focus:tw-border-2", + "focus:tw-border-primary-700", + "focus:tw-rounded-lg", + ]; + } + case false: { + return [ + "tw-cursor-pointer", + "tw-block", + "tw-rounded", + "tw-border", + "tw-border-solid", + "tw-border-secondary-300", + "hover:tw-border-text-main", + "focus:tw-border-2", + "focus:tw-border-primary-700", + ]; + } + } + } + + async ngOnInit(): Promise { + const teamsPlan = this.dialogParams.plans.find((plan) => plan.type === PlanType.TeamsMonthly); + const enterprisePlan = this.dialogParams.plans.find( + (plan) => plan.type === PlanType.EnterpriseMonthly, + ); + + this.planCards = [ + { + name: this.i18nService.t("planNameTeams"), + cost: teamsPlan.PasswordManager.seatPrice * 0.65, // 35% off for MSPs, + type: teamsPlan.type, + selected: true, + }, + { + name: this.i18nService.t("planNameEnterprise"), + cost: enterprisePlan.PasswordManager.seatPrice * 0.65, // 35% off for MSPs, + type: enterprisePlan.type, + selected: false, + }, + ]; + } + + protected selectPlan(name: string) { + this.planCards.find((planCard) => planCard.name === name).selected = true; + this.planCards.find((planCard) => planCard.name !== name).selected = false; + } + + submit = async () => { + this.formGroup.markAllAsTouched(); + + if (this.formGroup.invalid) { + return; + } + + const selectedPlanCard = this.planCards.find((planCard) => planCard.selected); + + await this.webProviderService.createClientOrganization( + this.dialogParams.providerId, + this.formGroup.value.organizationName, + this.formGroup.value.clientOwnerEmail, + selectedPlanCard.type, + this.formGroup.value.seats, + ); + + this.platformUtilsService.showToast("success", null, this.i18nService.t("createdNewClient")); + + this.dialogRef.close(this.ResultType.Submitted); + }; +} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts new file mode 100644 index 0000000000..fd9ef8296c --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts @@ -0,0 +1,3 @@ +export * from "./create-client-organization.component"; +export * from "./manage-client-organizations.component"; +export * from "./manage-client-organization-subscription.component"; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organization-subscription.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organization-subscription.component.ts index 2c8d59edc3..2182ac43ab 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organization-subscription.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organization-subscription.component.ts @@ -3,7 +3,7 @@ import { Component, Inject, OnInit } from "@angular/core"; import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction"; -import { ProviderSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/provider-subscription-update.request"; +import { UpdateClientOrganizationRequest } from "@bitwarden/common/billing/models/request/update-client-organization.request"; import { Plans } from "@bitwarden/common/billing/models/response/provider-subscription-response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -45,7 +45,7 @@ export class ManageClientOrganizationSubscriptionComponent implements OnInit { async ngOnInit() { try { - const response = await this.billingApiService.getProviderClientSubscriptions(this.providerId); + const response = await this.billingApiService.getProviderSubscription(this.providerId); this.AdditionalSeatPurchased = this.getPurchasedSeatsByPlan(this.planName, response.plans); const seatMinimum = this.getProviderSeatMinimumByPlan(this.planName, response.plans); const assignedByPlan = this.getAssignedByPlan(this.planName, response.plans); @@ -69,10 +69,10 @@ export class ManageClientOrganizationSubscriptionComponent implements OnInit { return; } - const request = new ProviderSubscriptionUpdateRequest(); + const request = new UpdateClientOrganizationRequest(); request.assignedSeats = assignedSeats; - await this.billingApiService.putProviderClientSubscriptions( + await this.billingApiService.updateClientOrganization( this.providerId, this.providerOrganizationId, request, diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.html index dc303d338f..ec5df609c4 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.html @@ -1,6 +1,12 @@ - + {{ "addNewOrganization" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.ts index 79dd25e891..2184a617cf 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.ts @@ -1,14 +1,16 @@ import { SelectionModel } from "@angular/cdk/collections"; -import { Component, OnInit } from "@angular/core"; +import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { firstValueFrom } from "rxjs"; -import { first } from "rxjs/operators"; +import { BehaviorSubject, firstValueFrom, from, lastValueFrom, Subject } from "rxjs"; +import { first, switchMap, takeUntil } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; +import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction"; +import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; @@ -16,6 +18,10 @@ import { DialogService, TableDataSource } from "@bitwarden/components"; import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; +import { + CreateClientOrganizationResultType, + openCreateClientOrganizationDialog, +} from "./create-client-organization.component"; import { ManageClientOrganizationSubscriptionComponent } from "./manage-client-organization-subscription.component"; @Component({ @@ -23,12 +29,22 @@ import { ManageClientOrganizationSubscriptionComponent } from "./manage-client-o }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class ManageClientOrganizationsComponent implements OnInit { +export class ManageClientOrganizationsComponent implements OnInit, OnDestroy { providerId: string; loading = true; manageOrganizations = false; + private destroy$ = new Subject(); + private _searchText$ = new BehaviorSubject(""); + private isSearching: boolean = false; + + get searchText() { + return this._searchText$.value; + } + set searchText(search: string) { + this._searchText$.value; + this.selection.clear(); this.dataSource.filter = search; } @@ -42,6 +58,7 @@ export class ManageClientOrganizationsComponent implements OnInit { private pagedClientsCount = 0; selection = new SelectionModel(true, []); protected dataSource = new TableDataSource(); + protected plans: PlanResponse[]; constructor( private route: ActivatedRoute, @@ -53,6 +70,7 @@ export class ManageClientOrganizationsComponent implements OnInit { private validationService: ValidationService, private webProviderService: WebProviderService, private dialogService: DialogService, + private billingApiService: BillingApiService, ) {} async ngOnInit() { @@ -67,20 +85,38 @@ export class ManageClientOrganizationsComponent implements OnInit { this.searchText = qParams.search; }); }); + + this._searchText$ + .pipe( + switchMap((searchText) => from(this.searchService.isSearchable(searchText))), + takeUntil(this.destroy$), + ) + .subscribe((isSearchable) => { + this.isSearching = isSearchable; + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); } async load() { - const response = await this.apiService.getProviderClients(this.providerId); - this.clients = response.data != null && response.data.length > 0 ? response.data : []; + const clientsResponse = await this.apiService.getProviderClients(this.providerId); + this.clients = + clientsResponse.data != null && clientsResponse.data.length > 0 ? clientsResponse.data : []; this.dataSource.data = this.clients; this.manageOrganizations = (await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin; + const plansResponse = await this.billingApiService.getPlans(); + this.plans = plansResponse.data; + this.loading = false; } isPaging() { - const searching = this.isSearching(); + const searching = this.isSearching; if (searching && this.didScroll) { // 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 @@ -89,10 +125,6 @@ export class ManageClientOrganizationsComponent implements OnInit { return !searching && this.clients && this.clients.length > this.pageSize; } - isSearching() { - return this.searchService.isSearchable(this.searchText); - } - async resetPaging() { this.pagedClients = []; this.loadMore(); @@ -157,4 +189,21 @@ export class ManageClientOrganizationsComponent implements OnInit { } this.actionPromise = null; } + + createClientOrganization = async () => { + const reference = openCreateClientOrganizationDialog(this.dialogService, { + data: { + providerId: this.providerId, + plans: this.plans, + }, + }); + + const result = await lastValueFrom(reference.closed); + + if (result === CreateClientOrganizationResultType.Closed) { + return; + } + + await this.load(); + }; } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.html new file mode 100644 index 0000000000..15b2519dae --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.html @@ -0,0 +1,29 @@ +
    +
    +
    + +
    +
    +
    +

    {{ name }}

    + + {{ linkText }} + + + {{ "new" | i18n }} + +
    +
    diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.spec.ts new file mode 100644 index 0000000000..94cec5f627 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.spec.ts @@ -0,0 +1,174 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { BehaviorSubject } from "rxjs"; + +import { SYSTEM_THEME_OBSERVABLE } from "../../../../../../../libs/angular/src/services/injection-tokens"; +import { ThemeType } from "../../../../../../../libs/common/src/platform/enums"; +import { ThemeStateService } from "../../../../../../../libs/common/src/platform/theming/theme-state.service"; + +import { IntegrationCardComponent } from "./integration-card.component"; + +describe("IntegrationCardComponent", () => { + let component: IntegrationCardComponent; + let fixture: ComponentFixture; + + const systemTheme$ = new BehaviorSubject(ThemeType.Light); + const usersPreferenceTheme$ = new BehaviorSubject(ThemeType.Light); + + beforeEach(async () => { + // reset system theme + systemTheme$.next(ThemeType.Light); + + await TestBed.configureTestingModule({ + declarations: [IntegrationCardComponent], + providers: [ + { + provide: ThemeStateService, + useValue: { selectedTheme$: usersPreferenceTheme$ }, + }, + { + provide: SYSTEM_THEME_OBSERVABLE, + useValue: systemTheme$, + }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(IntegrationCardComponent); + component = fixture.componentInstance; + + component.name = "Integration Name"; + component.image = "test-image.png"; + component.linkText = "Get started with integration"; + component.linkURL = "https://example.com/"; + + fixture.detectChanges(); + }); + + it("assigns link href", () => { + const link = fixture.nativeElement.querySelector("a"); + + expect(link.href).toBe("https://example.com/"); + }); + + it("renders card body", () => { + const name = fixture.nativeElement.querySelector("h3"); + const link = fixture.nativeElement.querySelector("a"); + + expect(name.textContent).toBe("Integration Name"); + expect(link.textContent.trim()).toBe("Get started with integration"); + }); + + it("assigns external rel attribute", () => { + component.externalURL = true; + fixture.detectChanges(); + + const link = fixture.nativeElement.querySelector("a"); + + expect(link.rel).toBe("noopener noreferrer"); + }); + + describe("new badge", () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date("2023-09-01")); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it("shows when expiration is in the future", () => { + component.newBadgeExpiration = "2023-09-02"; + expect(component.showNewBadge()).toBe(true); + }); + + it("does not show when expiration is not set", () => { + expect(component.showNewBadge()).toBe(false); + }); + + it("does not show when expiration is in the past", () => { + component.newBadgeExpiration = "2023-08-31"; + expect(component.showNewBadge()).toBe(false); + }); + + it("does not show when expiration is today", () => { + component.newBadgeExpiration = "2023-09-01"; + expect(component.showNewBadge()).toBe(false); + }); + + it("does not show when expiration is invalid", () => { + component.newBadgeExpiration = "not-a-date"; + expect(component.showNewBadge()).toBe(false); + }); + }); + + describe("imageDarkMode", () => { + it("ignores theme changes when darkModeImage is not set", () => { + systemTheme$.next(ThemeType.Dark); + usersPreferenceTheme$.next(ThemeType.Dark); + + fixture.detectChanges(); + + expect(component.imageEle.nativeElement.src).toContain("test-image.png"); + }); + + describe("user prefers the system theme", () => { + beforeEach(() => { + component.imageDarkMode = "test-image-dark.png"; + }); + + it("sets image src to imageDarkMode", () => { + usersPreferenceTheme$.next(ThemeType.System); + systemTheme$.next(ThemeType.Dark); + + fixture.detectChanges(); + + expect(component.imageEle.nativeElement.src).toContain("test-image-dark.png"); + }); + + it("sets image src to light mode image", () => { + component.imageEle.nativeElement.src = "test-image-dark.png"; + + usersPreferenceTheme$.next(ThemeType.System); + systemTheme$.next(ThemeType.Light); + + fixture.detectChanges(); + + expect(component.imageEle.nativeElement.src).toContain("test-image.png"); + }); + }); + + describe("user prefers dark mode", () => { + beforeEach(() => { + component.imageDarkMode = "test-image-dark.png"; + }); + + it("updates image to dark mode", () => { + systemTheme$.next(ThemeType.Light); // system theme shouldn't matter + usersPreferenceTheme$.next(ThemeType.Dark); + + fixture.detectChanges(); + + expect(component.imageEle.nativeElement.src).toContain("test-image-dark.png"); + }); + }); + + describe("user prefers light mode", () => { + beforeEach(() => { + component.imageDarkMode = "test-image-dark.png"; + }); + + it("updates image to light mode", () => { + component.imageEle.nativeElement.src = "test-image-dark.png"; + + systemTheme$.next(ThemeType.Dark); // system theme shouldn't matter + usersPreferenceTheme$.next(ThemeType.Light); + + fixture.detectChanges(); + + expect(component.imageEle.nativeElement.src).toContain("test-image.png"); + }); + }); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.ts new file mode 100644 index 0000000000..bf5f5bd311 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.ts @@ -0,0 +1,93 @@ +import { + AfterViewInit, + Component, + ElementRef, + Inject, + Input, + OnDestroy, + ViewChild, +} from "@angular/core"; +import { Observable, Subject, combineLatest, takeUntil } from "rxjs"; + +import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; +import { ThemeType } from "@bitwarden/common/platform/enums"; +import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; + +@Component({ + selector: "sm-integration-card", + templateUrl: "./integration-card.component.html", +}) +export class IntegrationCardComponent implements AfterViewInit, OnDestroy { + private destroyed$: Subject = new Subject(); + @ViewChild("imageEle") imageEle: ElementRef; + + @Input() name: string; + @Input() image: string; + @Input() imageDarkMode?: string; + @Input() linkText: string; + @Input() linkURL: string; + + /** Adds relevant `rel` attribute to external links */ + @Input() externalURL?: boolean; + + /** + * Date of when the new badge should be hidden. + * When omitted, the new badge is never shown. + * + * @example "2024-12-31" + */ + @Input() newBadgeExpiration?: string; + + constructor( + private themeStateService: ThemeStateService, + @Inject(SYSTEM_THEME_OBSERVABLE) + private systemTheme$: Observable, + ) {} + + ngAfterViewInit() { + combineLatest([this.themeStateService.selectedTheme$, this.systemTheme$]) + .pipe(takeUntil(this.destroyed$)) + .subscribe(([theme, systemTheme]) => { + // When the card doesn't have a dark mode image, exit early + if (!this.imageDarkMode) { + return; + } + + if (theme === ThemeType.System) { + // When the user's preference is the system theme, + // use the system theme to determine the image + const prefersDarkMode = + systemTheme === ThemeType.Dark || systemTheme === ThemeType.SolarizedDark; + + this.imageEle.nativeElement.src = prefersDarkMode ? this.imageDarkMode : this.image; + } else if (theme === ThemeType.Dark || theme === ThemeType.SolarizedDark) { + // When the user's preference is dark mode, use the dark mode image + this.imageEle.nativeElement.src = this.imageDarkMode; + } else { + // Otherwise use the light mode image + this.imageEle.nativeElement.src = this.image; + } + }); + } + + ngOnDestroy(): void { + this.destroyed$.next(); + this.destroyed$.complete(); + } + + /** Show the "new" badge when expiration is in the future */ + showNewBadge() { + if (!this.newBadgeExpiration) { + return false; + } + + const expirationDate = new Date(this.newBadgeExpiration); + + // Do not show the new badge for invalid dates + if (isNaN(expirationDate.getTime())) { + return false; + } + + return expirationDate > new Date(); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.html new file mode 100644 index 0000000000..a0c82d2f34 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.html @@ -0,0 +1,15 @@ +
      +
    • + +
    • +
    diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.spec.ts new file mode 100644 index 0000000000..e74e057e06 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.spec.ts @@ -0,0 +1,81 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { SYSTEM_THEME_OBSERVABLE } from "../../../../../../../libs/angular/src/services/injection-tokens"; +import { IntegrationType } from "../../../../../../../libs/common/src/enums"; +import { ThemeType } from "../../../../../../../libs/common/src/platform/enums"; +import { ThemeStateService } from "../../../../../../../libs/common/src/platform/theming/theme-state.service"; +import { IntegrationCardComponent } from "../integration-card/integration-card.component"; +import { Integration } from "../models/integration"; + +import { IntegrationGridComponent } from "./integration-grid.component"; + +describe("IntegrationGridComponent", () => { + let component: IntegrationGridComponent; + let fixture: ComponentFixture; + const integrations: Integration[] = [ + { + name: "Integration 1", + image: "test-image1.png", + linkText: "Get started with integration 1", + linkURL: "https://example.com/1", + type: IntegrationType.Integration, + }, + { + name: "SDK 2", + image: "test-image2.png", + linkText: "View SDK 2", + linkURL: "https://example.com/2", + type: IntegrationType.SDK, + }, + ]; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [IntegrationGridComponent, IntegrationCardComponent], + providers: [ + { + provide: ThemeStateService, + useValue: mock(), + }, + { + provide: SYSTEM_THEME_OBSERVABLE, + useValue: of(ThemeType.Light), + }, + ], + }); + + fixture = TestBed.createComponent(IntegrationGridComponent); + component = fixture.componentInstance; + component.integrations = integrations; + fixture.detectChanges(); + }); + + it("lists all integrations", () => { + expect(component.integrations).toEqual(integrations); + + const cards = fixture.debugElement.queryAll(By.directive(IntegrationCardComponent)); + + expect(cards.length).toBe(integrations.length); + }); + + it("assigns the correct attributes to IntegrationCardComponent", () => { + expect(component.integrations).toEqual(integrations); + + const card = fixture.debugElement.queryAll(By.directive(IntegrationCardComponent))[1]; + + expect(card.componentInstance.name).toBe("SDK 2"); + expect(card.componentInstance.image).toBe("test-image2.png"); + expect(card.componentInstance.linkText).toBe("View SDK 2"); + expect(card.componentInstance.linkURL).toBe("https://example.com/2"); + }); + + it("assigns `externalURL` for SDKs", () => { + const card = fixture.debugElement.queryAll(By.directive(IntegrationCardComponent)); + + expect(card[0].componentInstance.externalURL).toBe(false); + expect(card[1].componentInstance.externalURL).toBe(true); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.ts new file mode 100644 index 0000000000..058d59d702 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.ts @@ -0,0 +1,15 @@ +import { Component, Input } from "@angular/core"; + +import { IntegrationType } from "@bitwarden/common/enums"; + +import { Integration } from "../models/integration"; + +@Component({ + selector: "sm-integration-grid", + templateUrl: "./integration-grid.component.html", +}) +export class IntegrationGridComponent { + @Input() integrations: Integration[]; + + protected IntegrationType = IntegrationType; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations-routing.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations-routing.module.ts new file mode 100644 index 0000000000..91402113a9 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations-routing.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; + +import { IntegrationsComponent } from "./integrations.component"; + +const routes: Routes = [ + { + path: "", + component: IntegrationsComponent, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class IntegrationsRoutingModule {} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.html new file mode 100644 index 0000000000..a2f2188861 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.html @@ -0,0 +1,16 @@ + + + + +
    +

    {{ "integrationsDesc" | i18n }}

    + +
    + +
    +

    + {{ "sdks" | i18n }} +

    +

    {{ "sdksDesc" | i18n }}

    + +
    diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts new file mode 100644 index 0000000000..10fbaa1f3f --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts @@ -0,0 +1,77 @@ +import { Component } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { SYSTEM_THEME_OBSERVABLE } from "../../../../../../libs/angular/src/services/injection-tokens"; +import { I18nService } from "../../../../../../libs/common/src/platform/abstractions/i18n.service"; +import { ThemeType } from "../../../../../../libs/common/src/platform/enums"; +import { ThemeStateService } from "../../../../../../libs/common/src/platform/theming/theme-state.service"; +import { I18nPipe } from "../../../../../../libs/components/src/shared/i18n.pipe"; + +import { IntegrationCardComponent } from "./integration-card/integration-card.component"; +import { IntegrationGridComponent } from "./integration-grid/integration-grid.component"; +import { IntegrationsComponent } from "./integrations.component"; + +@Component({ + selector: "app-header", + template: "
    ", +}) +class MockHeaderComponent {} + +@Component({ + selector: "sm-new-menu", + template: "
    ", +}) +class MockNewMenuComponent {} + +describe("IntegrationsComponent", () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + IntegrationsComponent, + IntegrationGridComponent, + IntegrationCardComponent, + MockHeaderComponent, + MockNewMenuComponent, + I18nPipe, + ], + providers: [ + { + provide: I18nService, + useValue: mock({ t: (key) => key }), + }, + { + provide: ThemeStateService, + useValue: mock(), + }, + { + provide: SYSTEM_THEME_OBSERVABLE, + useValue: of(ThemeType.Light), + }, + ], + }).compileComponents(); + fixture = TestBed.createComponent(IntegrationsComponent); + fixture.detectChanges(); + }); + + it("divides Integrations & SDKS", () => { + const [integrationList, sdkList] = fixture.debugElement.queryAll( + By.directive(IntegrationGridComponent), + ); + + // Validate only expected names, as the data is constant + expect( + (integrationList.componentInstance as IntegrationGridComponent).integrations.map( + (i) => i.name, + ), + ).toEqual(["GitHub Actions", "GitLab CI/CD", "Ansible"]); + + expect( + (sdkList.componentInstance as IntegrationGridComponent).integrations.map((i) => i.name), + ).toEqual(["C#", "C++", "Go", "Java", "JS WebAssembly", "php", "Python", "Ruby"]); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts new file mode 100644 index 0000000000..f11048b6a3 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts @@ -0,0 +1,113 @@ +import { Component } from "@angular/core"; + +import { IntegrationType } from "@bitwarden/common/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { Integration } from "./models/integration"; + +@Component({ + selector: "sm-integrations", + templateUrl: "./integrations.component.html", +}) +export class IntegrationsComponent { + private integrationsAndSdks: Integration[] = []; + + constructor(i18nService: I18nService) { + this.integrationsAndSdks = [ + { + name: "GitHub Actions", + linkText: i18nService.t("setUpGithubActions"), + linkURL: "https://bitwarden.com/help/github-actions-integration/", + image: "../../../../../../../images/secrets-manager/integrations/github.svg", + imageDarkMode: "../../../../../../../images/secrets-manager/integrations/github-white.svg", + type: IntegrationType.Integration, + }, + { + name: "GitLab CI/CD", + linkText: i18nService.t("setUpGitlabCICD"), + linkURL: "https://bitwarden.com/help/gitlab-integration/", + image: "../../../../../../../images/secrets-manager/integrations/gitlab.svg", + imageDarkMode: "../../../../../../../images/secrets-manager/integrations/gitlab-white.svg", + type: IntegrationType.Integration, + }, + { + name: "Ansible", + linkText: i18nService.t("setUpAnsible"), + linkURL: "https://bitwarden.com/help/ansible-integration/", + image: "../../../../../../../images/secrets-manager/integrations/ansible.svg", + type: IntegrationType.Integration, + }, + { + name: "C#", + linkText: i18nService.t("cSharpSDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/csharp", + image: "../../../../../../../images/secrets-manager/sdks/c-sharp.svg", + type: IntegrationType.SDK, + }, + { + name: "C++", + linkText: i18nService.t("cPlusPlusSDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/cpp", + image: "../../../../../../../images/secrets-manager/sdks/c-plus-plus.png", + type: IntegrationType.SDK, + }, + { + name: "Go", + linkText: i18nService.t("goSDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/go", + image: "../../../../../../../images/secrets-manager/sdks/go.svg", + type: IntegrationType.SDK, + }, + { + name: "Java", + linkText: i18nService.t("javaSDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/java", + image: "../../../../../../../images/secrets-manager/sdks/java.svg", + imageDarkMode: "../../../../../../../images/secrets-manager/sdks/java-white.svg", + type: IntegrationType.SDK, + }, + { + name: "JS WebAssembly", + linkText: i18nService.t("jsWebAssemblySDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/js", + image: "../../../../../../../images/secrets-manager/sdks/wasm.svg", + type: IntegrationType.SDK, + }, + { + name: "php", + linkText: i18nService.t("phpSDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/php", + image: "../../../../../../../images/secrets-manager/sdks/php.svg", + type: IntegrationType.SDK, + }, + { + name: "Python", + linkText: i18nService.t("pythonSDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/python", + image: "../../../../../../../images/secrets-manager/sdks/python.svg", + type: IntegrationType.SDK, + }, + { + name: "Ruby", + linkText: i18nService.t("rubySDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/ruby", + image: "../../../../../../../images/secrets-manager/sdks/ruby.png", + type: IntegrationType.SDK, + }, + ]; + } + + /** Filter out content for the integrations sections */ + get integrations(): Integration[] { + return this.integrationsAndSdks.filter( + (integration) => integration.type === IntegrationType.Integration, + ); + } + + /** Filter out content for the SDKs section */ + get sdks(): Integration[] { + return this.integrationsAndSdks.filter( + (integration) => integration.type === IntegrationType.SDK, + ); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts new file mode 100644 index 0000000000..0d26b626f1 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from "@angular/core"; + +import { SecretsManagerSharedModule } from "../shared/sm-shared.module"; + +import { IntegrationCardComponent } from "./integration-card/integration-card.component"; +import { IntegrationGridComponent } from "./integration-grid/integration-grid.component"; +import { IntegrationsRoutingModule } from "./integrations-routing.module"; +import { IntegrationsComponent } from "./integrations.component"; + +@NgModule({ + imports: [SecretsManagerSharedModule, IntegrationsRoutingModule], + declarations: [IntegrationsComponent, IntegrationGridComponent, IntegrationCardComponent], + providers: [], +}) +export class IntegrationsModule {} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/models/integration.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/models/integration.ts new file mode 100644 index 0000000000..51ca79b30f --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/models/integration.ts @@ -0,0 +1,21 @@ +import { IntegrationType } from "@bitwarden/common/enums"; + +/** Integration or SDK */ +export type Integration = { + name: string; + image: string; + /** + * Optional image shown in dark mode. + */ + imageDarkMode?: string; + linkURL: string; + linkText: string; + type: IntegrationType; + /** + * Shows the "New" badge until the defined date. + * When omitted, the badge is never shown. + * + * @example "2024-12-31" + */ + newBadgeExpiration?: string; +}; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html index 5ac76a31fc..e382fbd9a9 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html @@ -18,8 +18,14 @@ > + >; -const SM_ONBOARDING_TASKS_KEY = new KeyDefinition(SM_ONBOARDING_DISK, "tasks", { - deserializer: (b) => b, -}); +const SM_ONBOARDING_TASKS_KEY = new UserKeyDefinition( + SM_ONBOARDING_DISK, + "tasks", + { + deserializer: (b) => b, + clearOn: [], // Used to track tasks completed by a user, we don't want to reshow if they've locked or logged out and came back to the app + }, +); @Injectable({ providedIn: "root", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.html index 2755377d2a..443711fd36 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.html @@ -1,14 +1,14 @@

    - {{ "projectServiceAccountsDescription" | i18n }} + {{ "projectMachineAccountsDescription" | i18n }}

    {{ "secrets" | i18n }} {{ "people" | i18n }} - {{ "serviceAccounts" | i18n }} + {{ "machineAccounts" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts index a5248c509f..6078520989 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts @@ -30,7 +30,7 @@ const routes: Routes = [ component: ProjectPeopleComponent, }, { - path: "service-accounts", + path: "machine-accounts", component: ProjectServiceAccountsComponent, }, ], diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.html index 9af3483703..5ef2be8ade 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.html @@ -8,7 +8,7 @@ {{ data.serviceAccounts.length }} - {{ "serviceAccounts" | i18n }} + {{ "machineAccounts" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.ts index 3d136aa92a..b31ef03d12 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.ts @@ -43,14 +43,14 @@ export class ServiceAccountDeleteDialogComponent { get title() { return this.data.serviceAccounts.length === 1 - ? this.i18nService.t("deleteServiceAccount") - : this.i18nService.t("deleteServiceAccounts"); + ? this.i18nService.t("deleteMachineAccount") + : this.i18nService.t("deleteMachineAccounts"); } get dialogContent() { return this.data.serviceAccounts.length === 1 - ? this.i18nService.t("deleteServiceAccountDialogMessage", this.data.serviceAccounts[0].name) - : this.i18nService.t("deleteServiceAccountsDialogMessage"); + ? this.i18nService.t("deleteMachineAccountDialogMessage", this.data.serviceAccounts[0].name) + : this.i18nService.t("deleteMachineAccountsDialogMessage"); } get dialogConfirmationLabel() { @@ -79,17 +79,17 @@ export class ServiceAccountDeleteDialogComponent { const message = this.data.serviceAccounts.length === 1 - ? "deleteServiceAccountToast" - : "deleteServiceAccountsToast"; + ? "deleteMachineAccountToast" + : "deleteMachineAccountsToast"; this.platformUtilsService.showToast("success", null, this.i18nService.t(message)); } openBulkStatusDialog(bulkStatusResults: BulkOperationStatus[]) { this.dialogService.open(BulkStatusDialogComponent, { data: { - title: "deleteServiceAccounts", - subTitle: "serviceAccounts", - columnTitle: "serviceAccountName", + title: "deleteMachineAccounts", + subTitle: "machineAccounts", + columnTitle: "machineAccountName", message: "bulkDeleteProjectsErrorMessage", details: bulkStatusResults, }, @@ -100,7 +100,7 @@ export class ServiceAccountDeleteDialogComponent { return this.data.serviceAccounts?.length === 1 ? this.i18nService.t("deleteProjectConfirmMessage", this.data.serviceAccounts[0].name) : this.i18nService.t( - "deleteServiceAccountsConfirmMessage", + "deleteMachineAccountsConfirmMessage", this.data.serviceAccounts?.length.toString(), ); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.html index 55f6ff4da1..0064341537 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.html @@ -7,7 +7,7 @@
    - {{ "serviceAccountName" | i18n }} + {{ "machineAccountName" | i18n }}
    diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts index 9aa7c658f3..105ca59e57 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts @@ -69,7 +69,7 @@ export class ServiceAccountDialogComponent { this.platformUtilsService.showToast( "error", null, - this.i18nService.t("serviceAccountsCannotCreate"), + this.i18nService.t("machineAccountsCannotCreate"), ); return; } @@ -85,14 +85,14 @@ export class ServiceAccountDialogComponent { if (this.data.operation == OperationType.Add) { await this.serviceAccountService.create(this.data.organizationId, serviceAccountView); - serviceAccountMessage = this.i18nService.t("serviceAccountCreated"); + serviceAccountMessage = this.i18nService.t("machineAccountCreated"); } else { await this.serviceAccountService.update( this.data.serviceAccountId, this.data.organizationId, serviceAccountView, ); - serviceAccountMessage = this.i18nService.t("serviceAccountUpdated"); + serviceAccountMessage = this.i18nService.t("machineAccountUpdated"); } this.platformUtilsService.showToast("success", null, serviceAccountMessage); @@ -107,6 +107,6 @@ export class ServiceAccountDialogComponent { } get title() { - return this.data.operation === OperationType.Add ? "newServiceAccount" : "editServiceAccount"; + return this.data.operation === OperationType.Add ? "newMachineAccount" : "editMachineAccount"; } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts index 1ef71811a1..554e7fa37d 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts @@ -65,7 +65,7 @@ export class ServiceAccountEventsComponent extends BaseEventsComponent implement protected getUserName() { return { - name: this.i18nService.t("serviceAccount") + " " + this.serviceAccountId, + name: this.i18nService.t("machineAccount") + " " + this.serviceAccountId, email: "", }; } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.ts index 6258f6f9db..c474ec44d5 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.ts @@ -21,8 +21,8 @@ export const serviceAccountAccessGuard: CanActivateFn = async (route: ActivatedR return createUrlTreeFromSnapshot(route, [ "/sm", route.params.organizationId, - "service-accounts", + "machine-accounts", ]); } - return createUrlTreeFromSnapshot(route, ["/sm", route.params.organizationId, "service-accounts"]); + return createUrlTreeFromSnapshot(route, ["/sm", route.params.organizationId, "machine-accounts"]); }; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.html index 79c8132bbc..074fa8ca00 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.html @@ -1,7 +1,7 @@

    - {{ "serviceAccountPeopleDescription" | i18n }} + {{ "machineAccountPeopleDescription" | i18n }}

    { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/sm", this.organizationId, "service-accounts"]); + this.router.navigate(["/sm", this.organizationId, "machine-accounts"]); return EMPTY; }), ); @@ -131,7 +131,7 @@ export class ServiceAccountPeopleComponent implements OnInit, OnDestroy { this.platformUtilsService.showToast( "success", null, - this.i18nService.t("serviceAccountAccessUpdated"), + this.i18nService.t("machineAccountAccessUpdated"), ); } catch (e) { this.validationService.showError(e); @@ -200,7 +200,7 @@ export class ServiceAccountPeopleComponent implements OnInit, OnDestroy { if (showAccessRemovalWarning) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["sm", this.organizationId, "service-accounts"]); + this.router.navigate(["sm", this.organizationId, "machine-accounts"]); } else if ( this.accessPolicySelectorService.isAccessRemoval(currentAccessPolicies, selectedPolicies) ) { @@ -210,8 +210,8 @@ export class ServiceAccountPeopleComponent implements OnInit, OnDestroy { private async showWarning(): Promise { const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "smAccessRemovalWarningSaTitle" }, - content: { key: "smAccessRemovalWarningSaMessage" }, + title: { key: "smAccessRemovalWarningMaTitle" }, + content: { key: "smAccessRemovalWarningMaMessage" }, acceptButtonText: { key: "removeAccess" }, cancelButtonText: { key: "cancel" }, type: "warning", @@ -222,7 +222,7 @@ export class ServiceAccountPeopleComponent implements OnInit, OnDestroy { private async showAccessTokenStillAvailableWarning(): Promise { await this.dialogService.openSimpleDialog({ title: { key: "saPeopleWarningTitle" }, - content: { key: "saPeopleWarningMessage" }, + content: { key: "maPeopleWarningMessage" }, type: "warning", acceptButtonText: { key: "close" }, cancelButtonText: null, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html index 368a62a933..b97c5ef114 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html @@ -1,6 +1,6 @@

    - {{ "serviceAccountProjectsDescription" | i18n }} + {{ "machineAccountProjectsDescription" | i18n }}

    {{ - "serviceAccounts" | i18n + "machineAccounts" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.ts index d352e8a246..bb687c51c6 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.ts @@ -45,11 +45,11 @@ export class ServiceAccountComponent implements OnInit, OnDestroy { catchError(() => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/sm", this.organizationId, "service-accounts"]).then(() => { + this.router.navigate(["/sm", this.organizationId, "machine-accounts"]).then(() => { this.platformUtilsService.showToast( "error", null, - this.i18nService.t("notFound", this.i18nService.t("serviceAccount")), + this.i18nService.t("notFound", this.i18nService.t("machineAccount")), ); }); return EMPTY; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.html index fb8d953e10..bfb7b98542 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.html @@ -3,8 +3,8 @@
    - {{ "serviceAccountsNoItemsTitle" | i18n }} - {{ "serviceAccountsNoItemsMessage" | i18n }} + {{ "machineAccountsNoItemsTitle" | i18n }} + {{ "machineAccountsNoItemsMessage" | i18n }} @@ -80,16 +80,16 @@ - {{ "viewServiceAccount" | i18n }} + {{ "viewMachineAccount" | i18n }} @@ -101,7 +101,7 @@ diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.html index 92ebcdbaac..d7a4f2c747 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.html @@ -1,6 +1,6 @@ diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/new-menu.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/new-menu.component.html index 528514e678..457eff37fa 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/new-menu.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/new-menu.component.html @@ -19,6 +19,6 @@ diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/sm-routing.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/sm-routing.module.ts index 0cad3129a4..00ec259a12 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/sm-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/sm-routing.module.ts @@ -2,10 +2,10 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; import { AuthGuard } from "@bitwarden/angular/auth/guards"; -import { buildFlaggedRoute } from "@bitwarden/web-vault/app/oss-routing.module"; import { organizationEnabledGuard } from "./guards/sm-org-enabled.guard"; import { canActivateSM } from "./guards/sm.guard"; +import { IntegrationsModule } from "./integrations/integrations.module"; import { LayoutComponent } from "./layout/layout.component"; import { NavigationComponent } from "./layout/navigation.component"; import { OverviewModule } from "./overview/overview.module"; @@ -17,7 +17,7 @@ import { OrgSuspendedComponent } from "./shared/org-suspended.component"; import { TrashModule } from "./trash/trash.module"; const routes: Routes = [ - buildFlaggedRoute("secretsManager", { + { path: "", children: [ { @@ -55,10 +55,17 @@ const routes: Routes = [ }, }, { - path: "service-accounts", + path: "machine-accounts", loadChildren: () => ServiceAccountsModule, data: { - titleId: "serviceAccounts", + titleId: "machineAccounts", + }, + }, + { + path: "integrations", + loadChildren: () => IntegrationsModule, + data: { + titleId: "integrations", }, }, { @@ -86,7 +93,7 @@ const routes: Routes = [ ], }, ], - }), + }, ]; @NgModule({ diff --git a/libs/angular/jest.config.js b/libs/angular/jest.config.js index e294e4ff47..c8e748575c 100644 --- a/libs/angular/jest.config.js +++ b/libs/angular/jest.config.js @@ -10,7 +10,11 @@ module.exports = { displayName: "libs/angular tests", preset: "jest-preset-angular", setupFilesAfterEnv: ["/test.setup.ts"], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "/", - }), + moduleNameMapper: pathsToModuleNameMapper( + // lets us use @bitwarden/common/spec in tests + { "@bitwarden/common/spec": ["../common/spec"], ...(compilerOptions?.paths ?? {}) }, + { + prefix: "/", + }, + ), }; diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index 167fe0a97f..5f8c4145cb 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -59,7 +59,7 @@ export class CollectionsComponent implements OnInit { } } - async submit() { + async submit(): Promise { const selectedCollectionIds = this.collections .filter((c) => { if (this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { @@ -75,7 +75,7 @@ export class CollectionsComponent implements OnInit { this.i18nService.t("errorOccurred"), this.i18nService.t("selectOneCollection"), ); - return; + return false; } this.cipherDomain.collectionIds = selectedCollectionIds; try { @@ -83,8 +83,10 @@ export class CollectionsComponent implements OnInit { await this.formPromise; this.onSavedCollections.emit(); this.platformUtilsService.showToast("success", null, this.i18nService.t("editedItem")); + return true; } catch (e) { this.logService.error(e); + return false; } } diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index aa3b801ded..9c2ed55357 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -12,6 +12,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; @@ -56,6 +57,7 @@ export class LockComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); constructor( + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected router: Router, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, @@ -206,6 +208,7 @@ export class LockComponent implements OnInit, OnDestroy { } private async doUnlockWithMasterPassword() { + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; const kdf = await this.stateService.getKdfType(); const kdfConfig = await this.stateService.getKdfConfig(); @@ -215,11 +218,13 @@ export class LockComponent implements OnInit, OnDestroy { kdf, kdfConfig, ); - const storedPasswordHash = await this.cryptoService.getMasterKeyHash(); + const storedMasterKeyHash = await firstValueFrom( + this.masterPasswordService.masterKeyHash$(userId), + ); let passwordValid = false; - if (storedPasswordHash != null) { + if (storedMasterKeyHash != null) { // Offline unlock possible passwordValid = await this.cryptoService.compareAndUpdateKeyHash( this.masterPassword, @@ -244,7 +249,7 @@ export class LockComponent implements OnInit, OnDestroy { masterKey, HashPurpose.LocalAuthorization, ); - await this.cryptoService.setMasterKeyHash(localKeyHash); + await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId); } catch (e) { this.logService.error(e); } finally { @@ -262,7 +267,7 @@ export class LockComponent implements OnInit, OnDestroy { } const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); - await this.cryptoService.setMasterKey(masterKey); + await this.masterPasswordService.setMasterKey(masterKey, userId); await this.setUserKeyAndContinue(userKey, true); } @@ -278,7 +283,6 @@ export class LockComponent implements OnInit, OnDestroy { } private async doContinue(evaluatePasswordAfterUnlock: boolean) { - await this.stateService.setEverBeenUnlocked(true); await this.biometricStateService.resetUserPromptCancelled(); this.messagingService.send("unlocked"); @@ -292,8 +296,10 @@ export class LockComponent implements OnInit, OnDestroy { } if (this.requirePasswordChange()) { - await this.stateService.setForceSetPasswordReason( + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( ForceSetPasswordReason.WeakMasterPassword, + userId, ); // 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 diff --git a/libs/angular/src/auth/components/login-via-auth-request.component.ts b/libs/angular/src/auth/components/login-via-auth-request.component.ts index 6ba94d3001..5a1180cd38 100644 --- a/libs/angular/src/auth/components/login-via-auth-request.component.ts +++ b/libs/angular/src/auth/components/login-via-auth-request.component.ts @@ -33,6 +33,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; +import { UserId } from "@bitwarden/common/types/guid"; import { CaptchaProtectedComponent } from "./captcha-protected.component"; @@ -131,6 +132,7 @@ export class LoginViaAuthRequestComponent // This also prevents it from being lost on refresh as the // login service email does not persist. this.email = await this.stateService.getEmail(); + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; if (!this.email) { this.platformUtilsService.showToast("error", null, this.i18nService.t("userEmailMissing")); @@ -142,10 +144,10 @@ export class LoginViaAuthRequestComponent // We only allow a single admin approval request to be active at a time // so must check state to see if we have an existing one or not - const adminAuthReqStorable = await this.stateService.getAdminAuthRequest(); + const adminAuthReqStorable = await this.authRequestService.getAdminAuthRequest(userId); if (adminAuthReqStorable) { - await this.handleExistingAdminAuthRequest(adminAuthReqStorable); + await this.handleExistingAdminAuthRequest(adminAuthReqStorable, userId); } else { // No existing admin auth request; so we need to create one await this.startAuthRequestLogin(); @@ -173,7 +175,10 @@ export class LoginViaAuthRequestComponent this.destroy$.complete(); } - private async handleExistingAdminAuthRequest(adminAuthReqStorable: AdminAuthRequestStorable) { + private async handleExistingAdminAuthRequest( + adminAuthReqStorable: AdminAuthRequestStorable, + userId: UserId, + ) { // Note: on login, the SSOLoginStrategy will also call to see an existing admin auth req // has been approved and handle it if so. @@ -183,13 +188,13 @@ export class LoginViaAuthRequestComponent adminAuthReqResponse = await this.apiService.getAuthRequest(adminAuthReqStorable.id); } catch (error) { if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) { - return await this.handleExistingAdminAuthReqDeletedOrDenied(); + return await this.handleExistingAdminAuthReqDeletedOrDenied(userId); } } // Request doesn't exist anymore if (!adminAuthReqResponse) { - return await this.handleExistingAdminAuthReqDeletedOrDenied(); + return await this.handleExistingAdminAuthReqDeletedOrDenied(userId); } // Re-derive the user's fingerprint phrase @@ -203,7 +208,7 @@ export class LoginViaAuthRequestComponent // Request denied if (adminAuthReqResponse.isAnswered && !adminAuthReqResponse.requestApproved) { - return await this.handleExistingAdminAuthReqDeletedOrDenied(); + return await this.handleExistingAdminAuthReqDeletedOrDenied(userId); } // Request approved @@ -211,6 +216,7 @@ export class LoginViaAuthRequestComponent return await this.handleApprovedAdminAuthRequest( adminAuthReqResponse, adminAuthReqStorable.privateKey, + userId, ); } @@ -219,9 +225,9 @@ export class LoginViaAuthRequestComponent await this.anonymousHubService.createHubConnection(adminAuthReqStorable.id); } - private async handleExistingAdminAuthReqDeletedOrDenied() { + private async handleExistingAdminAuthReqDeletedOrDenied(userId: UserId) { // clear the admin auth request from state - await this.stateService.setAdminAuthRequest(null); + await this.authRequestService.clearAdminAuthRequest(userId); // start new auth request // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -269,7 +275,8 @@ export class LoginViaAuthRequestComponent privateKey: this.authRequestKeyPair.privateKey, }); - await this.stateService.setAdminAuthRequest(adminAuthReqStorable); + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + await this.authRequestService.setAdminAuthRequest(adminAuthReqStorable, userId); } else { await this.buildAuthRequest(AuthRequestType.AuthenticateAndUnlock); reqResponse = await this.apiService.postAuthRequest(this.authRequest); @@ -333,9 +340,11 @@ export class LoginViaAuthRequestComponent // if user has authenticated via SSO if (this.userAuthNStatus === AuthenticationStatus.Locked) { + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; return await this.handleApprovedAdminAuthRequest( authReqResponse, this.authRequestKeyPair.privateKey, + userId, ); } @@ -363,6 +372,7 @@ export class LoginViaAuthRequestComponent async handleApprovedAdminAuthRequest( adminAuthReqResponse: AuthRequestResponse, privateKey: ArrayBuffer, + userId: UserId, ) { // See verifyAndHandleApprovedAuthReq(...) for flow details // it's flow 2 or 3 based on presence of masterPasswordHash @@ -384,7 +394,7 @@ export class LoginViaAuthRequestComponent // clear the admin auth request from state so it cannot be used again (it's a one time use) // TODO: this should eventually be enforced via deleting this on the server once it is used - await this.stateService.setAdminAuthRequest(null); + await this.authRequestService.clearAdminAuthRequest(userId); this.platformUtilsService.showToast("success", null, this.i18nService.t("loginApproved")); diff --git a/libs/angular/src/auth/components/set-password.component.ts b/libs/angular/src/auth/components/set-password.component.ts index a7442f711b..eebf87655b 100644 --- a/libs/angular/src/auth/components/set-password.component.ts +++ b/libs/angular/src/auth/components/set-password.component.ts @@ -12,6 +12,8 @@ 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 { OrganizationAutoEnrollStatusResponse } from "@bitwarden/common/admin-console/models/response/organization-auto-enroll-status.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; @@ -29,6 +31,7 @@ import { import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; +import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService } from "@bitwarden/components"; @@ -45,11 +48,14 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { resetPasswordAutoEnroll = false; onSuccessfulChangePassword: () => Promise; successRoute = "vault"; + userId: UserId; forceSetPasswordReason: ForceSetPasswordReason = ForceSetPasswordReason.None; ForceSetPasswordReason = ForceSetPasswordReason; constructor( + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, i18nService: I18nService, cryptoService: CryptoService, messagingService: MessagingService, @@ -88,7 +94,11 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { await this.syncService.fullSync(true); this.syncLoading = false; - this.forceSetPasswordReason = await this.stateService.getForceSetPasswordReason(); + this.userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + + this.forceSetPasswordReason = await firstValueFrom( + this.masterPasswordService.forceSetPasswordReason$(this.userId), + ); this.route.queryParams .pipe( @@ -176,7 +186,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { if (response == null) { throw new Error(this.i18nService.t("resetPasswordOrgKeysError")); } - const userId = await this.stateService.getUserId(); const publicKey = Utils.fromB64ToArray(response.publicKey); // RSA Encrypt user key with organization public key @@ -189,7 +198,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { return this.organizationUserService.putOrganizationUserResetPasswordEnrollment( this.orgId, - userId, + this.userId, resetRequest, ); }); @@ -226,7 +235,10 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { keyPair: [string, EncString] | null, ) { // Clear force set password reason to allow navigation back to vault. - await this.stateService.setForceSetPasswordReason(ForceSetPasswordReason.None); + await this.masterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.None, + this.userId, + ); // User now has a password so update account decryption options in state const userDecryptionOpts = await firstValueFrom( @@ -237,7 +249,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { await this.stateService.setKdfType(this.kdf); await this.stateService.setKdfConfig(this.kdfConfig); - await this.cryptoService.setMasterKey(masterKey); + await this.masterPasswordService.setMasterKey(masterKey, this.userId); await this.cryptoService.setUserKey(userKey[0]); // Set private key only for new JIT provisioned users in MP encryption orgs @@ -255,6 +267,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { masterKey, HashPurpose.LocalAuthorization, ); - await this.cryptoService.setMasterKeyHash(localMasterKeyHash); + await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.userId); } } diff --git a/libs/angular/src/auth/components/sso.component.spec.ts b/libs/angular/src/auth/components/sso.component.spec.ts index c5c062d9a7..269ec51e30 100644 --- a/libs/angular/src/auth/components/sso.component.spec.ts +++ b/libs/angular/src/auth/components/sso.component.spec.ts @@ -12,10 +12,13 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -23,7 +26,9 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; +import { UserId } from "@bitwarden/common/types/guid"; import { SsoComponent } from "./sso.component"; // test component that extends the SsoComponent @@ -48,6 +53,7 @@ describe("SsoComponent", () => { let component: TestSsoComponent; let _component: SsoComponentProtected; let fixture: ComponentFixture; + const userId = "userId" as UserId; // Mock Services let mockLoginStrategyService: MockProxy; @@ -67,6 +73,8 @@ describe("SsoComponent", () => { let mockLogService: MockProxy; let mockUserDecryptionOptionsService: MockProxy; let mockConfigService: MockProxy; + let mockMasterPasswordService: FakeMasterPasswordService; + let mockAccountService: FakeAccountService; // Mock authService.logIn params let code: string; @@ -117,6 +125,8 @@ describe("SsoComponent", () => { mockLogService = mock(); mockUserDecryptionOptionsService = mock(); mockConfigService = mock(); + mockAccountService = mockAccountServiceWith(userId); + mockMasterPasswordService = new FakeMasterPasswordService(); // Mock loginStrategyService.logIn params code = "code"; @@ -199,6 +209,8 @@ describe("SsoComponent", () => { }, { provide: LogService, useValue: mockLogService }, { provide: ConfigService, useValue: mockConfigService }, + { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, + { provide: AccountService, useValue: mockAccountService }, ], }); @@ -365,8 +377,9 @@ describe("SsoComponent", () => { await _component.logIn(code, codeVerifier, orgIdFromState); expect(mockLoginStrategyService.logIn).toHaveBeenCalledTimes(1); - expect(mockStateService.setForceSetPasswordReason).toHaveBeenCalledWith( + expect(mockMasterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, + userId, ); expect(mockOnSuccessfulLoginTdeNavigate).not.toHaveBeenCalled(); diff --git a/libs/angular/src/auth/components/sso.component.ts b/libs/angular/src/auth/components/sso.component.ts index 68d6e72e8d..30815beef8 100644 --- a/libs/angular/src/auth/components/sso.component.ts +++ b/libs/angular/src/auth/components/sso.component.ts @@ -11,6 +11,8 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -66,6 +68,8 @@ export class SsoComponent { protected logService: LogService, protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected configService: ConfigService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, + protected accountService: AccountService, ) {} async ngOnInit() { @@ -290,8 +294,10 @@ export class SsoComponent { // Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device) // Note: we cannot directly navigate in this scenario as we are in a pre-decryption state, and // if you try to set a new MP before decrypting, you will invalidate the user's data by making a new user key. - await this.stateService.setForceSetPasswordReason( + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, + userId, ); } diff --git a/libs/angular/src/auth/components/two-factor.component.spec.ts b/libs/angular/src/auth/components/two-factor.component.spec.ts index bff39188ea..0eb248f6d9 100644 --- a/libs/angular/src/auth/components/two-factor.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor.component.spec.ts @@ -15,11 +15,14 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -27,6 +30,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { TwoFactorComponent } from "./two-factor.component"; @@ -46,6 +51,7 @@ describe("TwoFactorComponent", () => { let _component: TwoFactorComponentProtected; let fixture: ComponentFixture; + const userId = "userId" as UserId; // Mock Services let mockLoginStrategyService: MockProxy; @@ -63,6 +69,8 @@ describe("TwoFactorComponent", () => { let mockUserDecryptionOptionsService: MockProxy; let mockSsoLoginService: MockProxy; let mockConfigService: MockProxy; + let mockMasterPasswordService: FakeMasterPasswordService; + let mockAccountService: FakeAccountService; let mockUserDecryptionOpts: { noMasterPassword: UserDecryptionOptions; @@ -93,6 +101,8 @@ describe("TwoFactorComponent", () => { mockUserDecryptionOptionsService = mock(); mockSsoLoginService = mock(); mockConfigService = mock(); + mockAccountService = mockAccountServiceWith(userId); + mockMasterPasswordService = new FakeMasterPasswordService(); mockUserDecryptionOpts = { noMasterPassword: new UserDecryptionOptions({ @@ -170,6 +180,8 @@ describe("TwoFactorComponent", () => { }, { provide: SsoLoginServiceAbstraction, useValue: mockSsoLoginService }, { provide: ConfigService, useValue: mockConfigService }, + { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, + { provide: AccountService, useValue: mockAccountService }, ], }); @@ -407,9 +419,9 @@ describe("TwoFactorComponent", () => { await component.doSubmit(); // Assert - - expect(mockStateService.setForceSetPasswordReason).toHaveBeenCalledWith( + expect(mockMasterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, + userId, ); expect(mockRouter.navigate).toHaveBeenCalledTimes(1); diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index c306e6cc80..f73f0483be 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -14,6 +14,8 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; @@ -92,6 +94,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction, protected configService: ConfigService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, + protected accountService: AccountService, ) { super(environmentService, i18nService, platformUtilsService); this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); @@ -342,8 +346,10 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI // Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device) // Note: we cannot directly navigate to the set password screen in this scenario as we are in a pre-decryption state, and // if you try to set a new MP before decrypting, you will invalidate the user's data by making a new user key. - await this.stateService.setForceSetPasswordReason( + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, + userId, ); } diff --git a/libs/angular/src/auth/components/update-temp-password.component.ts b/libs/angular/src/auth/components/update-temp-password.component.ts index 0b4541fe52..54fdc83239 100644 --- a/libs/angular/src/auth/components/update-temp-password.component.ts +++ b/libs/angular/src/auth/components/update-temp-password.component.ts @@ -1,9 +1,12 @@ import { Directive } from "@angular/core"; import { Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -56,6 +59,8 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { private userVerificationService: UserVerificationService, protected router: Router, dialogService: DialogService, + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, ) { super( i18nService, @@ -72,7 +77,8 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { async ngOnInit() { await this.syncService.fullSync(true); - this.reason = await this.stateService.getForceSetPasswordReason(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + this.reason = await firstValueFrom(this.masterPasswordService.forceSetPasswordReason$(userId)); // If we somehow end up here without a reason, go back to the home page if (this.reason == ForceSetPasswordReason.None) { @@ -163,7 +169,11 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { this.i18nService.t("updatedMasterPassword"), ); - await this.stateService.setForceSetPasswordReason(ForceSetPasswordReason.None); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.None, + userId, + ); if (this.onSuccessfulChangePassword != null) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/libs/angular/src/auth/guards/auth.guard.ts b/libs/angular/src/auth/guards/auth.guard.ts index 29024cfa0b..b8e37d0af3 100644 --- a/libs/angular/src/auth/guards/auth.guard.ts +++ b/libs/angular/src/auth/guards/auth.guard.ts @@ -1,12 +1,14 @@ import { Injectable } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router"; +import { firstValueFrom } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @Injectable() export class AuthGuard implements CanActivate { @@ -15,7 +17,8 @@ export class AuthGuard implements CanActivate { private router: Router, private messagingService: MessagingService, private keyConnectorService: KeyConnectorService, - private stateService: StateService, + private accountService: AccountService, + private masterPasswordService: MasterPasswordServiceAbstraction, ) {} async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) { @@ -40,7 +43,10 @@ export class AuthGuard implements CanActivate { return this.router.createUrlTree(["/remove-password"]); } - const forceSetPasswordReason = await this.stateService.getForceSetPasswordReason(); + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + const forceSetPasswordReason = await firstValueFrom( + this.masterPasswordService.forceSetPasswordReason$(userId), + ); if ( forceSetPasswordReason === diff --git a/libs/angular/src/auth/guards/unauth.guard.ts b/libs/angular/src/auth/guards/unauth.guard.ts index 35c59b5744..9e1bca98ca 100644 --- a/libs/angular/src/auth/guards/unauth.guard.ts +++ b/libs/angular/src/auth/guards/unauth.guard.ts @@ -2,7 +2,6 @@ import { Injectable, inject } from "@angular/core"; import { CanActivate, CanActivateFn, Router, UrlTree } from "@angular/router"; import { Observable, map } from "rxjs"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -43,14 +42,14 @@ const defaultRoutes: UnauthRoutes = { }; function unauthGuard(routes: UnauthRoutes): Observable { - const accountService = inject(AccountService); + const authService = inject(AuthService); const router = inject(Router); - return accountService.activeAccount$.pipe( - map((accountData) => { - if (accountData == null || accountData.status === AuthenticationStatus.LoggedOut) { + return authService.activeAccountStatus$.pipe( + map((status) => { + if (status == null || status === AuthenticationStatus.LoggedOut) { return true; - } else if (accountData.status === AuthenticationStatus.Locked) { + } else if (status === AuthenticationStatus.Locked) { return router.createUrlTree([routes.locked]); } else { return router.createUrlTree([routes.homepage()]); diff --git a/libs/angular/src/auth/icons/create-passkey-failed.icon.ts b/libs/angular/src/auth/icons/create-passkey-failed.icon.ts index 39a2389c5a..65902a64c9 100644 --- a/libs/angular/src/auth/icons/create-passkey-failed.icon.ts +++ b/libs/angular/src/auth/icons/create-passkey-failed.icon.ts @@ -2,27 +2,27 @@ import { svgIcon } from "@bitwarden/components"; export const CreatePasskeyFailedIcon = svgIcon` - - - - - - - - - `; diff --git a/libs/angular/src/auth/icons/create-passkey.icon.ts b/libs/angular/src/auth/icons/create-passkey.icon.ts index c0e984bbee..79ba4021b5 100644 --- a/libs/angular/src/auth/icons/create-passkey.icon.ts +++ b/libs/angular/src/auth/icons/create-passkey.icon.ts @@ -2,25 +2,25 @@ import { svgIcon } from "@bitwarden/components"; export const CreatePasskeyIcon = svgIcon` - - - - - - - - `; diff --git a/libs/angular/src/components/toastr.component.ts b/libs/angular/src/components/toastr.component.ts deleted file mode 100644 index bfe20ed866..0000000000 --- a/libs/angular/src/components/toastr.component.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { animate, state, style, transition, trigger } from "@angular/animations"; -import { CommonModule } from "@angular/common"; -import { Component, ModuleWithProviders, NgModule } from "@angular/core"; -import { - DefaultNoComponentGlobalConfig, - GlobalConfig, - Toast as BaseToast, - ToastPackage, - ToastrService, - TOAST_CONFIG, -} from "ngx-toastr"; - -@Component({ - selector: "[toast-component2]", - template: ` - -
    - -
    -
    -
    - {{ title }} [{{ duplicatesCount + 1 }}] -
    -
    -
    - {{ message }} -
    -
    -
    -
    -
    - `, - animations: [ - trigger("flyInOut", [ - state("inactive", style({ opacity: 0 })), - state("active", style({ opacity: 1 })), - state("removed", style({ opacity: 0 })), - transition("inactive => active", animate("{{ easeTime }}ms {{ easing }}")), - transition("active => removed", animate("{{ easeTime }}ms {{ easing }}")), - ]), - ], - preserveWhitespaces: false, -}) -export class BitwardenToast extends BaseToast { - constructor( - protected toastrService: ToastrService, - public toastPackage: ToastPackage, - ) { - super(toastrService, toastPackage); - } -} - -export const BitwardenToastGlobalConfig: GlobalConfig = { - ...DefaultNoComponentGlobalConfig, - toastComponent: BitwardenToast, -}; - -@NgModule({ - imports: [CommonModule], - declarations: [BitwardenToast], - exports: [BitwardenToast], -}) -export class BitwardenToastModule { - static forRoot(config: Partial = {}): ModuleWithProviders { - return { - ngModule: BitwardenToastModule, - providers: [ - { - provide: TOAST_CONFIG, - useValue: { - default: BitwardenToastGlobalConfig, - config: config, - }, - }, - ], - }; - } -} diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index 64fb44e3b8..5f1bf796aa 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -2,10 +2,9 @@ import { CommonModule, DatePipe } from "@angular/common"; import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { AutofocusDirective } from "@bitwarden/components"; +import { AutofocusDirective, ToastModule } from "@bitwarden/components"; import { CalloutComponent } from "./components/callout.component"; -import { BitwardenToastModule } from "./components/toastr.component"; import { A11yInvalidDirective } from "./directives/a11y-invalid.directive"; import { A11yTitleDirective } from "./directives/a11y-title.directive"; import { ApiActionDirective } from "./directives/api-action.directive"; @@ -34,7 +33,7 @@ import { IconComponent } from "./vault/components/icon.component"; @NgModule({ imports: [ - BitwardenToastModule.forRoot({ + ToastModule.forRoot({ maxOpened: 5, autoDismiss: true, closeButton: true, @@ -77,7 +76,7 @@ import { IconComponent } from "./vault/components/icon.component"; A11yTitleDirective, ApiActionDirective, AutofocusDirective, - BitwardenToastModule, + ToastModule, BoxRowDirective, CalloutComponent, CopyTextDirective, diff --git a/libs/angular/src/platform/services/broadcaster.service.ts b/libs/angular/src/platform/services/broadcaster.service.ts deleted file mode 100644 index cf58d2b311..0000000000 --- a/libs/angular/src/platform/services/broadcaster.service.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Injectable } from "@angular/core"; - -import { BroadcasterService as BaseBroadcasterService } from "@bitwarden/common/platform/services/broadcaster.service"; - -@Injectable() -export class BroadcasterService extends BaseBroadcasterService {} diff --git a/libs/angular/src/platform/utils/safe-provider.ts b/libs/angular/src/platform/utils/safe-provider.ts index 7c19a280d6..e7547f9b82 100644 --- a/libs/angular/src/platform/utils/safe-provider.ts +++ b/libs/angular/src/platform/utils/safe-provider.ts @@ -85,9 +85,25 @@ type SafeConcreteProvider< deps: D; }; +/** + * If useAngularDecorators: true is specified, do not require a deps array. + * This is a manual override for where @Injectable decorators are used + */ +type UseAngularDecorators = Omit & { + useAngularDecorators: true; +}; + +/** + * Represents a type with a deps array that may optionally be overridden with useAngularDecorators + */ +type AllowAngularDecorators = T | UseAngularDecorators; + /** * A factory function that creates a provider for the ngModule providers array. - * This guarantees type safety for your provider definition. It does nothing at runtime. + * This (almost) guarantees type safety for your provider definition. It does nothing at runtime. + * Warning: the useAngularDecorators option provides an override where your class uses the Injectable decorator, + * however this cannot be enforced by the type system and will not cause an error if the decorator is not used. + * @example safeProvider({ provide: MyService, useClass: DefaultMyService, deps: [AnotherService] }) * @param provider Your provider object in the usual shape (e.g. using useClass, useValue, useFactory, etc.) * @returns The exact same object without modification (pass-through). */ @@ -113,10 +129,10 @@ export const safeProvider = < DConcrete extends MapParametersToDeps>, >( provider: - | SafeClassProvider + | AllowAngularDecorators> | SafeValueProvider - | SafeFactoryProvider + | AllowAngularDecorators> | SafeExistingProvider - | SafeConcreteProvider + | AllowAngularDecorators> | Constructor, ): SafeProvider => provider as SafeProvider; diff --git a/libs/angular/src/platform/utils/safe-provider.type.spec.ts b/libs/angular/src/platform/utils/safe-provider.type.spec.ts new file mode 100644 index 0000000000..6fe6d0d0b6 --- /dev/null +++ b/libs/angular/src/platform/utils/safe-provider.type.spec.ts @@ -0,0 +1,111 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +// This rule bans @ts-expect-error comments without explanation. In this file, we use it to test our types, and +// explanation is provided in header comments before each test. + +import { safeProvider } from "./safe-provider"; + +class FooFactory { + create() { + return "thing"; + } +} + +abstract class FooService { + createFoo: (str: string) => string; +} + +class DefaultFooService implements FooService { + constructor(private factory: FooFactory) {} + + createFoo(str: string) { + return str ?? this.factory.create(); + } +} + +class BarFactory { + create() { + return 5; + } +} + +abstract class BarService { + createBar: (num: number) => number; +} + +class DefaultBarService implements BarService { + constructor(private factory: BarFactory) {} + + createBar(num: number) { + return num ?? this.factory.create(); + } +} + +abstract class FooBarService {} + +class DefaultFooBarService { + constructor( + private fooFactory: FooFactory, + private barFactory: BarFactory, + ) {} +} + +// useClass happy path with deps +safeProvider({ + provide: FooService, + useClass: DefaultFooService, + deps: [FooFactory], +}); + +// useClass happy path with useAngularDecorators +safeProvider({ + provide: FooService, + useClass: DefaultFooService, + useAngularDecorators: true, +}); + +// useClass: expect error if implementation does not match abstraction +safeProvider({ + provide: FooService, + // @ts-expect-error + useClass: DefaultBarService, + deps: [BarFactory], +}); + +// useClass: expect error if deps type does not match +safeProvider({ + provide: FooService, + useClass: DefaultFooService, + // @ts-expect-error + deps: [BarFactory], +}); + +// useClass: expect error if not enough deps specified +safeProvider({ + provide: FooService, + useClass: DefaultFooService, + // @ts-expect-error + deps: [], +}); + +// useClass: expect error if too many deps specified +safeProvider({ + provide: FooService, + useClass: DefaultFooService, + // @ts-expect-error + deps: [FooFactory, BarFactory], +}); + +// useClass: expect error if deps are in the wrong order +safeProvider({ + provide: FooBarService, + useClass: DefaultFooBarService, + // @ts-expect-error + deps: [BarFactory, FooFactory], +}); + +// useClass: expect error if no deps specified and not using Angular decorators +// @ts-expect-error +safeProvider({ + provide: FooService, + useClass: DefaultFooService, +}); diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.svg b/libs/angular/src/scss/bwicons/fonts/bwi-font.svg index e815389126..bc0a348fee 100644 --- a/libs/angular/src/scss/bwicons/fonts/bwi-font.svg +++ b/libs/angular/src/scss/bwicons/fonts/bwi-font.svg @@ -5,11 +5,11 @@ - + @@ -154,7 +154,7 @@ - + @@ -187,6 +187,17 @@ + + + + + + + + + + + diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.ttf b/libs/angular/src/scss/bwicons/fonts/bwi-font.ttf index f9b63283e0..f70eea7af7 100644 Binary files a/libs/angular/src/scss/bwicons/fonts/bwi-font.ttf and b/libs/angular/src/scss/bwicons/fonts/bwi-font.ttf differ diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.woff b/libs/angular/src/scss/bwicons/fonts/bwi-font.woff index 1e57b1aab3..52cecc3ead 100644 Binary files a/libs/angular/src/scss/bwicons/fonts/bwi-font.woff and b/libs/angular/src/scss/bwicons/fonts/bwi-font.woff differ diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.woff2 b/libs/angular/src/scss/bwicons/fonts/bwi-font.woff2 index 88036b7b3e..4c8cfd6e04 100644 Binary files a/libs/angular/src/scss/bwicons/fonts/bwi-font.woff2 and b/libs/angular/src/scss/bwicons/fonts/bwi-font.woff2 differ diff --git a/libs/angular/src/scss/bwicons/styles/style.scss b/libs/angular/src/scss/bwicons/styles/style.scss index af13e0ddb6..e1333da468 100644 --- a/libs/angular/src/scss/bwicons/styles/style.scss +++ b/libs/angular/src/scss/bwicons/styles/style.scss @@ -104,10 +104,13 @@ $icons: ( "universal-access": "\e991", "save-changes": "\e988", "browser": "\e985", + "browser-alt": "\e9a3", "mobile": "\e986", + "mobile-alt": "\e9a4", "cli": "\e987", "providers": "\e983", "vault": "\e984", + "vault-f": "\e9ab", "folder-closed-f": "\e982", "rocket": "\e9ee", "ellipsis-h": "\e9ef", @@ -131,9 +134,11 @@ $icons: ( "hamburger": "\e972", "bw-folder-open-f1": "\e93e", "desktop": "\e96a", + "desktop-alt": "\e9a2", "angle-up": "\e969", "user": "\e900", "user-f": "\e901", + "user-monitor": "\e9a7", "key": "\e902", "share-square": "\e903", "hashtag": "\e904", @@ -157,6 +162,7 @@ $icons: ( "files": "\e916", "trash": "\e917", "plus": "\e918", + "plus-f": "\e9a9", "star": "\e919", "list": "\e91a", "angle-down": "\e92d", @@ -237,6 +243,7 @@ $icons: ( "linkedin": "\e955", "discourse": "\e91e", "twitter": "\e961", + "x-twitter": "\e9a5", "youtube": "\e966", "windows": "\e964", "apple": "\e945", @@ -265,6 +272,10 @@ $icons: ( "caret-down": "\e99e", "passkey": "\e99f", "lock-encrypted": "\e9a0", + "back": "\e9a8", + "popout": "\e9aa", + "wand": "\e9a6", + "msp": "\e9a1", ); @each $name, $glyph in $icons { diff --git a/libs/angular/src/services/injection-tokens.ts b/libs/angular/src/services/injection-tokens.ts index 7d39078797..6fffe722fb 100644 --- a/libs/angular/src/services/injection-tokens.ts +++ b/libs/angular/src/services/injection-tokens.ts @@ -1,5 +1,5 @@ import { InjectionToken } from "@angular/core"; -import { Observable } from "rxjs"; +import { Observable, Subject } from "rxjs"; import { AbstractMemoryStorageService, @@ -8,6 +8,7 @@ import { } from "@bitwarden/common/platform/abstractions/storage.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message } from "@bitwarden/common/platform/messaging"; declare const tag: unique symbol; /** @@ -49,3 +50,6 @@ export const LOG_MAC_FAILURES = new SafeInjectionToken("LOG_MAC_FAILURE export const SYSTEM_THEME_OBSERVABLE = new SafeInjectionToken>( "SYSTEM_THEME_OBSERVABLE", ); +export const INTRAPROCESS_MESSAGING_SUBJECT = new SafeInjectionToken>>( + "INTRAPROCESS_MESSAGING_SUBJECT", +); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 73f2bb4a32..9d311d34af 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1,4 +1,5 @@ import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core"; +import { Subject } from "rxjs"; import { AuthRequestServiceAbstraction, @@ -38,6 +39,7 @@ import { InternalPolicyService, PolicyService as PolicyServiceAbstraction, } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; @@ -47,9 +49,11 @@ import { DefaultOrganizationManagementPreferencesService } from "@bitwarden/comm import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation"; import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; +import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service"; import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service"; import { + AccountService, AccountService as AccountServiceAbstraction, InternalAccountService, } from "@bitwarden/common/auth/abstractions/account.service"; @@ -60,6 +64,10 @@ import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abst import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { + InternalMasterPasswordServiceAbstraction, + MasterPasswordServiceAbstraction, +} from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; @@ -78,6 +86,7 @@ import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; +import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { PasswordResetEnrollmentServiceImplementation } from "@bitwarden/common/auth/services/password-reset-enrollment.service.implementation"; import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; @@ -108,7 +117,7 @@ import { BillingApiService } from "@bitwarden/common/billing/services/billing-ap import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; import { PaymentMethodWarningsService } from "@bitwarden/common/billing/services/payment-method-warnings.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -129,6 +138,9 @@ import { DefaultBiometricStateService, } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- Used for dependency injection +import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { devFlagEnabled, flagEnabled } from "@bitwarden/common/platform/misc/flags"; import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; @@ -139,6 +151,7 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; +import { DefaultBroadcasterService } from "@bitwarden/common/platform/services/default-broadcaster.service"; import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; @@ -147,6 +160,7 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r import { NoopNotificationsService } from "@bitwarden/common/platform/services/noop-notifications.service"; import { StateService } from "@bitwarden/common/platform/services/state.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; +import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service"; import { ValidationService } from "@bitwarden/common/platform/services/validation.service"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { @@ -238,7 +252,6 @@ import { import { AuthGuard } from "../auth/guards/auth.guard"; import { UnauthGuard } from "../auth/guards/unauth.guard"; import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service"; -import { BroadcasterService } from "../platform/services/broadcaster.service"; import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service"; import { LoggingErrorHandler } from "../platform/services/logging-error-handler"; import { AngularThemingService } from "../platform/services/theming/angular-theming.service"; @@ -261,6 +274,7 @@ import { SYSTEM_LANGUAGE, SYSTEM_THEME_OBSERVABLE, WINDOW, + INTRAPROCESS_MESSAGING_SUBJECT, } from "./injection-tokens"; import { ModalService } from "./modal.service"; @@ -359,6 +373,8 @@ const safeProviders: SafeProvider[] = [ provide: LoginStrategyServiceAbstraction, useClass: LoginStrategyService, deps: [ + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, TokenServiceAbstraction, @@ -404,6 +420,7 @@ const safeProviders: SafeProvider[] = [ encryptService: EncryptService, fileUploadService: CipherFileUploadServiceAbstraction, configService: ConfigService, + stateProvider: StateProvider, ) => new CipherService( cryptoService, @@ -416,6 +433,7 @@ const safeProviders: SafeProvider[] = [ encryptService, fileUploadService, configService, + stateProvider, ), deps: [ CryptoServiceAbstraction, @@ -428,6 +446,7 @@ const safeProviders: SafeProvider[] = [ EncryptService, CipherFileUploadServiceAbstraction, ConfigService, + StateProvider, ], }), safeProvider({ @@ -437,7 +456,6 @@ const safeProviders: SafeProvider[] = [ CryptoServiceAbstraction, I18nServiceAbstraction, CipherServiceAbstraction, - StateServiceAbstraction, StateProvider, ], }), @@ -521,6 +539,7 @@ const safeProviders: SafeProvider[] = [ provide: CryptoServiceAbstraction, useClass: CryptoService, deps: [ + InternalMasterPasswordServiceAbstraction, KeyGenerationServiceAbstraction, CryptoFunctionServiceAbstraction, EncryptService, @@ -587,6 +606,8 @@ const safeProviders: SafeProvider[] = [ provide: SyncServiceAbstraction, useClass: SyncService, deps: [ + InternalMasterPasswordServiceAbstraction, + AccountServiceAbstraction, ApiServiceAbstraction, DomainSettingsService, InternalFolderService, @@ -607,9 +628,14 @@ const safeProviders: SafeProvider[] = [ AvatarServiceAbstraction, LOGOUT_CALLBACK, BillingAccountProfileStateService, + TokenServiceAbstraction, ], }), - safeProvider({ provide: BroadcasterServiceAbstraction, useClass: BroadcasterService, deps: [] }), + safeProvider({ + provide: BroadcasterService, + useClass: DefaultBroadcasterService, + deps: [MessageSender, MessageListener], + }), safeProvider({ provide: VaultTimeoutSettingsServiceAbstraction, useClass: VaultTimeoutSettingsService, @@ -626,6 +652,8 @@ const safeProviders: SafeProvider[] = [ provide: VaultTimeoutService, useClass: VaultTimeoutService, deps: [ + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, CipherServiceAbstraction, FolderServiceAbstraction, CollectionServiceAbstraction, @@ -714,7 +742,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: SearchServiceAbstraction, useClass: SearchService, - deps: [LogService, I18nServiceAbstraction], + deps: [LogService, I18nServiceAbstraction, StateProvider], }), safeProvider({ provide: NotificationsServiceAbstraction, @@ -728,6 +756,7 @@ const safeProviders: SafeProvider[] = [ LOGOUT_CALLBACK, StateServiceAbstraction, AuthServiceAbstraction, + AuthRequestServiceAbstraction, MessagingServiceAbstraction, ], }), @@ -744,7 +773,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: EventUploadServiceAbstraction, useClass: EventUploadService, - deps: [ApiServiceAbstraction, StateProvider, LogService, AccountServiceAbstraction], + deps: [ApiServiceAbstraction, StateProvider, LogService, AuthServiceAbstraction], }), safeProvider({ provide: EventCollectionServiceAbstraction, @@ -754,7 +783,7 @@ const safeProviders: SafeProvider[] = [ StateProvider, OrganizationServiceAbstraction, EventUploadServiceAbstraction, - AccountServiceAbstraction, + AuthServiceAbstraction, ], }), safeProvider({ @@ -771,10 +800,21 @@ const safeProviders: SafeProvider[] = [ useClass: PolicyApiService, deps: [InternalPolicyService, ApiServiceAbstraction], }), + safeProvider({ + provide: InternalMasterPasswordServiceAbstraction, + useClass: MasterPasswordService, + deps: [StateProvider], + }), + safeProvider({ + provide: MasterPasswordServiceAbstraction, + useExisting: InternalMasterPasswordServiceAbstraction, + }), safeProvider({ provide: KeyConnectorServiceAbstraction, useClass: KeyConnectorService, deps: [ + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, TokenServiceAbstraction, @@ -791,6 +831,8 @@ const safeProviders: SafeProvider[] = [ deps: [ StateServiceAbstraction, CryptoServiceAbstraction, + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, I18nServiceAbstraction, UserVerificationApiServiceAbstraction, UserDecryptionOptionsServiceAbstraction, @@ -934,9 +976,11 @@ const safeProviders: SafeProvider[] = [ useClass: AuthRequestService, deps: [ AppIdServiceAbstraction, + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, - StateServiceAbstraction, + StateProvider, ], }), safeProvider({ @@ -1019,10 +1063,13 @@ const safeProviders: SafeProvider[] = [ provide: OrganizationBillingServiceAbstraction, useClass: OrganizationBillingService, deps: [ + ApiServiceAbstraction, + BillingApiServiceAbstraction, CryptoServiceAbstraction, EncryptService, I18nServiceAbstraction, OrganizationApiServiceAbstraction, + SyncServiceAbstraction, ], }), safeProvider({ @@ -1080,11 +1127,36 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultOrganizationManagementPreferencesService, deps: [StateProvider], }), + safeProvider({ + provide: UserKeyInitService, + useClass: UserKeyInitService, + deps: [AccountService, CryptoServiceAbstraction, LogService], + }), safeProvider({ provide: ErrorHandler, useClass: LoggingErrorHandler, deps: [], }), + safeProvider({ + provide: INTRAPROCESS_MESSAGING_SUBJECT, + useFactory: () => new Subject>(), + deps: [], + }), + safeProvider({ + provide: MessageListener, + useFactory: (subject: Subject>) => new MessageListener(subject.asObservable()), + deps: [INTRAPROCESS_MESSAGING_SUBJECT], + }), + safeProvider({ + provide: MessageSender, + useFactory: (subject: Subject>) => new SubjectMessageSender(subject), + deps: [INTRAPROCESS_MESSAGING_SUBJECT], + }), + safeProvider({ + provide: ProviderApiServiceAbstraction, + useClass: ProviderApiService, + deps: [ApiServiceAbstraction], + }), ]; function encryptServiceFactory( diff --git a/libs/angular/src/services/unassigned-items-banner.api.service.ts b/libs/angular/src/services/unassigned-items-banner.api.service.ts new file mode 100644 index 0000000000..69b74f8c7f --- /dev/null +++ b/libs/angular/src/services/unassigned-items-banner.api.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; + +@Injectable({ providedIn: "root" }) +export class UnassignedItemsBannerApiService { + constructor(private apiService: ApiService) {} + + async getShowUnassignedCiphersBanner(): Promise { + const r = await this.apiService.send( + "GET", + "/ciphers/has-unassigned-ciphers", + null, + true, + true, + ); + return r; + } +} diff --git a/libs/angular/src/services/unassigned-items-banner.service.spec.ts b/libs/angular/src/services/unassigned-items-banner.service.spec.ts new file mode 100644 index 0000000000..bf0fb23881 --- /dev/null +++ b/libs/angular/src/services/unassigned-items-banner.service.spec.ts @@ -0,0 +1,65 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; + +import { UnassignedItemsBannerApiService } from "./unassigned-items-banner.api.service"; +import { SHOW_BANNER_KEY, UnassignedItemsBannerService } from "./unassigned-items-banner.service"; + +describe("UnassignedItemsBanner", () => { + let stateProvider: FakeStateProvider; + let apiService: MockProxy; + let environmentService: MockProxy; + let organizationService: MockProxy; + + const sutFactory = () => + new UnassignedItemsBannerService( + stateProvider, + apiService, + environmentService, + organizationService, + ); + + beforeEach(() => { + const fakeAccountService = mockAccountServiceWith("userId" as UserId); + stateProvider = new FakeStateProvider(fakeAccountService); + apiService = mock(); + environmentService = mock(); + environmentService.environment$ = of(null); + organizationService = mock(); + organizationService.organizations$ = of([]); + }); + + it("shows the banner if showBanner local state is true", async () => { + const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY); + showBanner.nextState(true); + + const sut = sutFactory(); + expect(await firstValueFrom(sut.showBanner$)).toBe(true); + expect(apiService.getShowUnassignedCiphersBanner).not.toHaveBeenCalled(); + }); + + it("does not show the banner if showBanner local state is false", async () => { + const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY); + showBanner.nextState(false); + + const sut = sutFactory(); + expect(await firstValueFrom(sut.showBanner$)).toBe(false); + expect(apiService.getShowUnassignedCiphersBanner).not.toHaveBeenCalled(); + }); + + it("fetches from server if local state has not been set yet", async () => { + apiService.getShowUnassignedCiphersBanner.mockResolvedValue(true); + + const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY); + showBanner.nextState(undefined); + + const sut = sutFactory(); + + expect(await firstValueFrom(sut.showBanner$)).toBe(true); + expect(apiService.getShowUnassignedCiphersBanner).toHaveBeenCalledTimes(1); + }); +}); diff --git a/libs/angular/src/services/unassigned-items-banner.service.ts b/libs/angular/src/services/unassigned-items-banner.service.ts new file mode 100644 index 0000000000..db93d4c4fc --- /dev/null +++ b/libs/angular/src/services/unassigned-items-banner.service.ts @@ -0,0 +1,87 @@ +import { Injectable } from "@angular/core"; +import { combineLatest, concatMap, map, startWith } from "rxjs"; + +import { + OrganizationService, + canAccessOrgAdmin, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + EnvironmentService, + Region, +} from "@bitwarden/common/platform/abstractions/environment.service"; +import { + StateProvider, + UNASSIGNED_ITEMS_BANNER_DISK, + UserKeyDefinition, +} from "@bitwarden/common/platform/state"; + +import { UnassignedItemsBannerApiService } from "./unassigned-items-banner.api.service"; + +export const SHOW_BANNER_KEY = new UserKeyDefinition( + UNASSIGNED_ITEMS_BANNER_DISK, + "showBanner", + { + deserializer: (b) => b, + clearOn: [], + }, +); + +/** Displays a banner that tells users how to move their unassigned items into a collection. */ +@Injectable({ providedIn: "root" }) +export class UnassignedItemsBannerService { + private _showBanner = this.stateProvider.getActive(SHOW_BANNER_KEY); + + showBanner$ = this._showBanner.state$.pipe( + concatMap(async (showBannerState) => { + // null indicates that the user has not seen or dismissed the banner yet - get the flag from server + if (showBannerState == null) { + const showBannerResponse = await this.apiService.getShowUnassignedCiphersBanner(); + await this._showBanner.update(() => showBannerResponse); + return showBannerResponse; + } + + return showBannerState; + }), + ); + + private adminConsoleOrg$ = this.organizationService.organizations$.pipe( + map((orgs) => orgs.find((o) => canAccessOrgAdmin(o))), + ); + + adminConsoleUrl$ = combineLatest([ + this.adminConsoleOrg$, + this.environmentService.environment$, + ]).pipe( + map(([org, environment]) => { + if (org == null || environment == null) { + return "#"; + } + + return environment.getWebVaultUrl() + "/#/organizations/" + org.id; + }), + ); + + bannerText$ = this.environmentService.environment$.pipe( + map((e) => + e?.getRegion() == Region.SelfHosted + ? "unassignedItemsBannerSelfHostNotice" + : "unassignedItemsBannerNotice", + ), + ); + + loading$ = combineLatest([this.adminConsoleUrl$, this.bannerText$]).pipe( + startWith(true), + map(() => false), + ); + + constructor( + private stateProvider: StateProvider, + private apiService: UnassignedItemsBannerApiService, + private environmentService: EnvironmentService, + private organizationService: OrganizationService, + ) {} + + async hideBanner() { + await this._showBanner.update(() => false); + } +} diff --git a/libs/angular/src/tools/send/send.component.ts b/libs/angular/src/tools/send/send.component.ts index 90d9b39e8c..fc51e32416 100644 --- a/libs/angular/src/tools/send/send.component.ts +++ b/libs/angular/src/tools/send/send.component.ts @@ -1,5 +1,13 @@ import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core"; -import { Subject, firstValueFrom, mergeMap, takeUntil } from "rxjs"; +import { + BehaviorSubject, + Subject, + firstValueFrom, + mergeMap, + from, + switchMap, + takeUntil, +} from "rxjs"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -24,7 +32,6 @@ export class SendComponent implements OnInit, OnDestroy { expired = false; type: SendType = null; sends: SendView[] = []; - searchText: string; selectedType: SendType; selectedAll: boolean; filter: (cipher: SendView) => boolean; @@ -39,6 +46,8 @@ export class SendComponent implements OnInit, OnDestroy { private searchTimeout: any; private destroy$ = new Subject(); private _filteredSends: SendView[]; + private _searchText$ = new BehaviorSubject(""); + protected isSearchable: boolean = false; get filteredSends(): SendView[] { return this._filteredSends; @@ -48,6 +57,14 @@ export class SendComponent implements OnInit, OnDestroy { this._filteredSends = filteredSends; } + get searchText() { + return this._searchText$.value; + } + + set searchText(value: string) { + this._searchText$.next(value); + } + constructor( protected sendService: SendService, protected i18nService: I18nService, @@ -68,6 +85,15 @@ export class SendComponent implements OnInit, OnDestroy { .subscribe((policyAppliesToActiveUser) => { this.disableSend = policyAppliesToActiveUser; }); + + this._searchText$ + .pipe( + switchMap((searchText) => from(this.searchService.isSearchable(searchText))), + takeUntil(this.destroy$), + ) + .subscribe((isSearchable) => { + this.isSearchable = isSearchable; + }); } ngOnDestroy() { @@ -122,14 +148,14 @@ export class SendComponent implements OnInit, OnDestroy { clearTimeout(this.searchTimeout); } if (timeout == null) { - this.hasSearched = this.searchService.isSearchable(this.searchText); + this.hasSearched = this.isSearchable; this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s)); this.applyTextSearch(); return; } this.searchPending = true; this.searchTimeout = setTimeout(async () => { - this.hasSearched = this.searchService.isSearchable(this.searchText); + this.hasSearched = this.isSearchable; this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s)); this.applyTextSearch(); this.searchPending = false; diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 36182ed9cf..d29c74b42d 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -1,6 +1,6 @@ import { DatePipe } from "@angular/common"; import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { concatMap, Observable, Subject, takeUntil } from "rxjs"; +import { concatMap, firstValueFrom, Observable, Subject, takeUntil } from "rxjs"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; @@ -592,7 +592,7 @@ export class AddEditComponent implements OnInit, OnDestroy { this.writeableCollections.forEach((c) => ((c as any).checked = false)); } if (this.cipher.organizationId != null) { - this.collections = this.writeableCollections.filter( + this.collections = this.writeableCollections?.filter( (c) => c.organizationId === this.cipher.organizationId, ); const org = await this.organizationService.get(this.cipher.organizationId); @@ -662,7 +662,7 @@ export class AddEditComponent implements OnInit, OnDestroy { // if a cipher is unassigned we want to check if they are an admin or have permission to edit any collection if (!cipher.collectionIds) { - orgAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled); + orgAdmin = this.organization?.canEditUnassignedCiphers(); } return this.cipher.id == null @@ -687,7 +687,7 @@ export class AddEditComponent implements OnInit, OnDestroy { } async loadAddEditCipherInfo(): Promise { - const addEditCipherInfo: any = await this.stateService.getAddEditCipherInfo(); + const addEditCipherInfo: any = await firstValueFrom(this.cipherService.addEditCipherInfo$); const loadedSavedInfo = addEditCipherInfo != null; if (loadedSavedInfo) { @@ -700,7 +700,7 @@ export class AddEditComponent implements OnInit, OnDestroy { } } - await this.stateService.setAddEditCipherInfo(null); + await this.cipherService.setAddEditCipherInfo(null); return loadedSavedInfo; } diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index cdfb1b6299..20e779e77c 100644 --- a/libs/angular/src/vault/components/vault-items.component.ts +++ b/libs/angular/src/vault/components/vault-items.component.ts @@ -1,4 +1,5 @@ -import { Directive, EventEmitter, Input, Output } from "@angular/core"; +import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; +import { BehaviorSubject, Subject, from, switchMap, takeUntil } from "rxjs"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -6,7 +7,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @Directive() -export class VaultItemsComponent { +export class VaultItemsComponent implements OnInit, OnDestroy { @Input() activeCipherId: string = null; @Output() onCipherClicked = new EventEmitter(); @Output() onCipherRightClicked = new EventEmitter(); @@ -23,13 +24,15 @@ export class VaultItemsComponent { protected searchPending = false; + private destroy$ = new Subject(); private searchTimeout: any = null; - private _searchText: string = null; + private isSearchable: boolean = false; + private _searchText$ = new BehaviorSubject(""); get searchText() { - return this._searchText; + return this._searchText$.value; } set searchText(value: string) { - this._searchText = value; + this._searchText$.next(value); } constructor( @@ -37,6 +40,22 @@ export class VaultItemsComponent { protected cipherService: CipherService, ) {} + ngOnInit(): void { + this._searchText$ + .pipe( + switchMap((searchText) => from(this.searchService.isSearchable(searchText))), + takeUntil(this.destroy$), + ) + .subscribe((isSearchable) => { + this.isSearchable = isSearchable; + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + async load(filter: (cipher: CipherView) => boolean = null, deleted = false) { this.deleted = deleted ?? false; await this.applyFilter(filter); @@ -90,7 +109,7 @@ export class VaultItemsComponent { } isSearching() { - return !this.searchPending && this.searchService.isSearchable(this.searchText); + return !this.searchPending && this.isSearchable; } protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted; diff --git a/libs/auth/src/angular/icons/user-verification-biometrics-fingerprint.icon.ts b/libs/auth/src/angular/icons/user-verification-biometrics-fingerprint.icon.ts index 1fb994fda1..f661f9330b 100644 --- a/libs/auth/src/angular/icons/user-verification-biometrics-fingerprint.icon.ts +++ b/libs/auth/src/angular/icons/user-verification-biometrics-fingerprint.icon.ts @@ -2,11 +2,11 @@ import { svgIcon } from "@bitwarden/components"; export const UserVerificationBiometricsIcon = svgIcon` - - - - - - + + + + + + `; diff --git a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts index 7af92fc8f8..b7ae903eac 100644 --- a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts +++ b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts @@ -1,12 +1,52 @@ import { Observable } from "rxjs"; +import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; +import { UserId } from "@bitwarden/common/types/guid"; import { UserKey, MasterKey } from "@bitwarden/common/types/key"; export abstract class AuthRequestServiceAbstraction { /** Emits an auth request id when an auth request has been approved. */ authRequestPushNotification$: Observable; + + /** + * Returns true if the user has chosen to allow auth requests to show on this client. + * Intended to prevent spamming the user with auth requests. + * @param userId The user id. + * @throws If `userId` is not provided. + */ + abstract getAcceptAuthRequests: (userId: UserId) => Promise; + /** + * Sets whether to allow auth requests to show on this client for this user. + * @param accept Whether to allow auth requests to show on this client. + * @param userId The user id. + * @throws If `userId` is not provided. + */ + abstract setAcceptAuthRequests: (accept: boolean, userId: UserId) => Promise; + /** + * Returns an admin auth request for the given user if it exists. + * @param userId The user id. + * @throws If `userId` is not provided. + */ + abstract getAdminAuthRequest: (userId: UserId) => Promise; + /** + * Sets an admin auth request for the given user. + * Note: use {@link clearAdminAuthRequest} to clear the request. + * @param authRequest The admin auth request. + * @param userId The user id. + * @throws If `authRequest` or `userId` is not provided. + */ + abstract setAdminAuthRequest: ( + authRequest: AdminAuthRequestStorable, + userId: UserId, + ) => Promise; + /** + * Clears an admin auth request for the given user. + * @param userId The user id. + * @throws If `userId` is not provided. + */ + abstract clearAdminAuthRequest: (userId: UserId) => Promise; /** * Approve or deny an auth request. * @param approve True to approve, false to deny. diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts index 53722cd259..0ce6c9fed7 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts @@ -5,6 +5,7 @@ import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abst import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -14,7 +15,9 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -42,6 +45,10 @@ describe("AuthRequestLoginStrategy", () => { let deviceTrustCryptoService: MockProxy; let billingAccountProfileStateService: MockProxy; + const mockUserId = Utils.newGuid() as UserId; + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; + let authRequestLoginStrategy: AuthRequestLoginStrategy; let credentials: AuthRequestLoginCredentials; let tokenResponse: IdentityTokenResponse; @@ -71,12 +78,17 @@ describe("AuthRequestLoginStrategy", () => { deviceTrustCryptoService = mock(); billingAccountProfileStateService = mock(); + accountService = mockAccountServiceWith(mockUserId); + masterPasswordService = new FakeMasterPasswordService(); + tokenService.getTwoFactorToken.mockResolvedValue(null); appIdService.getAppId.mockResolvedValue(deviceId); tokenService.decodeAccessToken.mockResolvedValue({}); authRequestLoginStrategy = new AuthRequestLoginStrategy( cache, + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -108,13 +120,16 @@ describe("AuthRequestLoginStrategy", () => { const masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey; const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await authRequestLoginStrategy.logIn(credentials); - expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey); - expect(cryptoService.setMasterKeyHash).toHaveBeenCalledWith(decMasterKeyHash); + expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(masterKey, mockUserId); + expect(masterPasswordService.mock.setMasterKeyHash).toHaveBeenCalledWith( + decMasterKeyHash, + mockUserId, + ); expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); expect(deviceTrustCryptoService.trustDeviceIfRequired).toHaveBeenCalled(); @@ -136,8 +151,8 @@ describe("AuthRequestLoginStrategy", () => { await authRequestLoginStrategy.logIn(credentials); // setMasterKey and setMasterKeyHash should not be called - expect(cryptoService.setMasterKey).not.toHaveBeenCalled(); - expect(cryptoService.setMasterKeyHash).not.toHaveBeenCalled(); + expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled(); + expect(masterPasswordService.mock.setMasterKeyHash).not.toHaveBeenCalled(); // setMasterKeyEncryptedUserKey, setUserKey, and setPrivateKey should still be called expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts index 31a0cebbfe..4035a7be58 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts @@ -1,8 +1,10 @@ -import { Observable, map, BehaviorSubject } from "rxjs"; +import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; @@ -47,6 +49,8 @@ export class AuthRequestLoginStrategy extends LoginStrategy { constructor( data: AuthRequestLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -61,6 +65,8 @@ export class AuthRequestLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -114,12 +120,22 @@ export class AuthRequestLoginStrategy extends LoginStrategy { authRequestCredentials.decryptedMasterKey && authRequestCredentials.decryptedMasterKeyHash ) { - await this.cryptoService.setMasterKey(authRequestCredentials.decryptedMasterKey); - await this.cryptoService.setMasterKeyHash(authRequestCredentials.decryptedMasterKeyHash); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setMasterKey( + authRequestCredentials.decryptedMasterKey, + userId, + ); + await this.masterPasswordService.setMasterKeyHash( + authRequestCredentials.decryptedMasterKeyHash, + userId, + ); } } - protected override async setUserKey(response: IdentityTokenResponse): Promise { + protected override async setUserKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise { const authRequestCredentials = this.cache.value.authRequestCredentials; // User now may or may not have a master password // but set the master key encrypted user key if it exists regardless @@ -130,14 +146,14 @@ export class AuthRequestLoginStrategy extends LoginStrategy { } else { await this.trySetUserKeyWithMasterKey(); - const userId = (await this.stateService.getUserId()) as UserId; // Establish trust if required after setting user key await this.deviceTrustCryptoService.trustDeviceIfRequired(userId); } } private async trySetUserKeyWithMasterKey(): Promise { - const masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (masterKey) { const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); await this.cryptoService.setUserKey(userKey); diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index 0ac22047c5..e0833342ce 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -14,6 +14,7 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response"; import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; @@ -26,16 +27,17 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Account, AccountProfile, - AccountTokens, AccountKeys, } from "@bitwarden/common/platform/models/domain/account"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction, PasswordStrengthService, } from "@bitwarden/common/tools/password-strength"; import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { UserId } from "@bitwarden/common/types/guid"; import { UserKey, MasterKey } from "@bitwarden/common/types/key"; import { LoginStrategyServiceAbstraction } from "../abstractions"; @@ -56,7 +58,7 @@ const privateKey = "PRIVATE_KEY"; const captchaSiteKey = "CAPTCHA_SITE_KEY"; const kdf = 0; const kdfIterations = 10000; -const userId = Utils.newGuid(); +const userId = Utils.newGuid() as UserId; const masterPasswordHash = "MASTER_PASSWORD_HASH"; const name = "NAME"; const defaultUserDecryptionOptionsServerResponse: IUserDecryptionOptionsServerResponse = { @@ -98,6 +100,8 @@ export function identityTokenResponseFactory( // TODO: add tests for latest changes to base class for TDE describe("LoginStrategy", () => { let cache: PasswordLoginStrategyData; + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; let loginStrategyService: MockProxy; let cryptoService: MockProxy; @@ -118,6 +122,9 @@ describe("LoginStrategy", () => { let credentials: PasswordLoginCredentials; beforeEach(async () => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); + loginStrategyService = mock(); cryptoService = mock(); apiService = mock(); @@ -139,6 +146,8 @@ describe("LoginStrategy", () => { // The base class is abstract so we test it via PasswordLoginStrategy passwordLoginStrategy = new PasswordLoginStrategy( cache, + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -203,9 +212,6 @@ describe("LoginStrategy", () => { kdfType: kdf, }, }, - tokens: { - ...new AccountTokens(), - }, keys: new AccountKeys(), }), ); @@ -241,7 +247,7 @@ describe("LoginStrategy", () => { }); apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); const result = await passwordLoginStrategy.logIn(credentials); @@ -260,7 +266,7 @@ describe("LoginStrategy", () => { cryptoService.makeKeyPair.mockResolvedValue(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]); apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await passwordLoginStrategy.logIn(credentials); @@ -382,6 +388,8 @@ describe("LoginStrategy", () => { passwordLoginStrategy = new PasswordLoginStrategy( cache, + accountService, + masterPasswordService, cryptoService, apiService, tokenService, diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index 4fe99b276c..a73c32e120 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -1,6 +1,8 @@ import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -25,11 +27,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { - Account, - AccountProfile, - AccountTokens, -} from "@bitwarden/common/platform/models/domain/account"; +import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account"; +import { UserId } from "@bitwarden/common/types/guid"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { @@ -60,6 +59,8 @@ export abstract class LoginStrategy { protected abstract cache: BehaviorSubject; constructor( + protected accountService: AccountService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected cryptoService: CryptoService, protected apiService: ApiService, protected tokenService: TokenService, @@ -156,16 +157,13 @@ export abstract class LoginStrategy { * @param {IdentityTokenResponse} tokenResponse - The response from the server containing the identity token. * @returns {Promise} - A promise that resolves when the account information has been successfully saved. */ - protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise { + protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise { const accountInformation = await this.tokenService.decodeAccessToken(tokenResponse.accessToken); const userId = accountInformation.sub; - // If you don't persist existing admin auth requests on login, they will get deleted. - const adminAuthRequest = await this.stateService.getAdminAuthRequest({ userId }); - - const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction(); - const vaultTimeout = await this.stateService.getVaultTimeout(); + const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId }); + const vaultTimeout = await this.stateService.getVaultTimeout({ userId }); // set access token and refresh token before account initialization so authN status can be accurate // User id will be derived from the access token. @@ -190,10 +188,6 @@ export abstract class LoginStrategy { kdfType: tokenResponse.kdf, }, }, - tokens: { - ...new AccountTokens(), - }, - adminAuthRequest: adminAuthRequest?.toJSON(), }), ); @@ -202,6 +196,7 @@ export abstract class LoginStrategy { ); await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false); + return userId as UserId; } protected async processTokenResponse(response: IdentityTokenResponse): Promise { @@ -224,7 +219,7 @@ export abstract class LoginStrategy { } // Must come before setting keys, user key needs email to update additional keys - await this.saveAccountInformation(response); + const userId = await this.saveAccountInformation(response); if (response.twoFactorToken != null) { // note: we can read email from access token b/c it was saved in saveAccountInformation @@ -234,7 +229,7 @@ export abstract class LoginStrategy { } await this.setMasterKey(response); - await this.setUserKey(response); + await this.setUserKey(response, userId); await this.setPrivateKey(response); this.messagingService.send("loggedIn"); @@ -244,7 +239,7 @@ export abstract class LoginStrategy { // The keys comes from different sources depending on the login strategy protected abstract setMasterKey(response: IdentityTokenResponse): Promise; - protected abstract setUserKey(response: IdentityTokenResponse): Promise; + protected abstract setUserKey(response: IdentityTokenResponse, userId: UserId): Promise; protected abstract setPrivateKey(response: IdentityTokenResponse): Promise; // Old accounts used master key for encryption. We are forcing migrations but only need to diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index 470a4ac713..b902fff574 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -9,6 +9,7 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -19,11 +20,13 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { HashPurpose } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction, PasswordStrengthService, } from "@bitwarden/common/tools/password-strength"; import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { LoginStrategyServiceAbstraction } from "../abstractions"; @@ -42,6 +45,7 @@ const masterKey = new SymmetricCryptoKey( "N2KWjlLpfi5uHjv+YcfUKIpZ1l+W+6HRensmIqD+BFYBf6N/dvFpJfWwYnVBdgFCK2tJTAIMLhqzIQQEUmGFgg==", ), ) as MasterKey; +const userId = Utils.newGuid() as UserId; const deviceId = Utils.newGuid(); const masterPasswordPolicy = new MasterPasswordPolicyResponse({ EnforceOnLogin: true, @@ -50,6 +54,8 @@ const masterPasswordPolicy = new MasterPasswordPolicyResponse({ describe("PasswordLoginStrategy", () => { let cache: PasswordLoginStrategyData; + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; let loginStrategyService: MockProxy; let cryptoService: MockProxy; @@ -71,6 +77,9 @@ describe("PasswordLoginStrategy", () => { let tokenResponse: IdentityTokenResponse; beforeEach(async () => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); + loginStrategyService = mock(); cryptoService = mock(); apiService = mock(); @@ -102,6 +111,8 @@ describe("PasswordLoginStrategy", () => { passwordLoginStrategy = new PasswordLoginStrategy( cache, + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -145,13 +156,16 @@ describe("PasswordLoginStrategy", () => { it("sets keys after a successful authentication", async () => { const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await passwordLoginStrategy.logIn(credentials); - expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey); - expect(cryptoService.setMasterKeyHash).toHaveBeenCalledWith(localHashedPassword); + expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(masterKey, userId); + expect(masterPasswordService.mock.setMasterKeyHash).toHaveBeenCalledWith( + localHashedPassword, + userId, + ); expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey); @@ -183,8 +197,9 @@ describe("PasswordLoginStrategy", () => { const result = await passwordLoginStrategy.logIn(credentials); expect(policyService.evaluateMasterPassword).toHaveBeenCalled(); - expect(stateService.setForceSetPasswordReason).toHaveBeenCalledWith( + expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( ForceSetPasswordReason.WeakMasterPassword, + userId, ); expect(result.forcePasswordReset).toEqual(ForceSetPasswordReason.WeakMasterPassword); }); @@ -222,8 +237,9 @@ describe("PasswordLoginStrategy", () => { expect(firstResult.forcePasswordReset).toEqual(ForceSetPasswordReason.None); // Second login attempt should save the force password reset options and return in result - expect(stateService.setForceSetPasswordReason).toHaveBeenCalledWith( + expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( ForceSetPasswordReason.WeakMasterPassword, + userId, ); expect(secondResult.forcePasswordReset).toEqual(ForceSetPasswordReason.WeakMasterPassword); }); diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index d3de3ea6ba..2490c35a00 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -1,9 +1,11 @@ -import { BehaviorSubject, map, Observable } from "rxjs"; +import { BehaviorSubject, firstValueFrom, map, Observable } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; @@ -23,6 +25,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { HashPurpose } from "@bitwarden/common/platform/enums"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey } from "@bitwarden/common/types/key"; import { LoginStrategyServiceAbstraction } from "../abstractions"; @@ -70,6 +73,8 @@ export class PasswordLoginStrategy extends LoginStrategy { constructor( data: PasswordLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -86,6 +91,8 @@ export class PasswordLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -157,8 +164,10 @@ export class PasswordLoginStrategy extends LoginStrategy { }); } else { // Authentication was successful, save the force update password options with the state service - await this.stateService.setForceSetPasswordReason( + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( ForceSetPasswordReason.WeakMasterPassword, + userId, ); authResult.forcePasswordReset = ForceSetPasswordReason.WeakMasterPassword; } @@ -184,7 +193,8 @@ export class PasswordLoginStrategy extends LoginStrategy { !result.requiresCaptcha && forcePasswordResetReason != ForceSetPasswordReason.None ) { - await this.stateService.setForceSetPasswordReason(forcePasswordResetReason); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason(forcePasswordResetReason, userId); result.forcePasswordReset = forcePasswordResetReason; } @@ -193,18 +203,22 @@ export class PasswordLoginStrategy extends LoginStrategy { protected override async setMasterKey(response: IdentityTokenResponse) { const { masterKey, localMasterKeyHash } = this.cache.value; - await this.cryptoService.setMasterKey(masterKey); - await this.cryptoService.setMasterKeyHash(localMasterKeyHash); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setMasterKey(masterKey, userId); + await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId); } - protected override async setUserKey(response: IdentityTokenResponse): Promise { + protected override async setUserKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise { // If migration is required, we won't have a user key to set yet. if (this.encryptionKeyMigrationRequired(response)) { return; } await this.cryptoService.setMasterKeyEncryptedUserKey(response.key); - const masterKey = await this.cryptoService.getMasterKey(); + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (masterKey) { const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); await this.cryptoService.setUserKey(userKey); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index d4b0b13eaf..b78ad6dea6 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts @@ -9,6 +9,7 @@ import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/a import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; @@ -20,7 +21,9 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { UserId } from "@bitwarden/common/types/guid"; import { DeviceKey, UserKey, MasterKey } from "@bitwarden/common/types/key"; import { @@ -33,6 +36,9 @@ import { identityTokenResponseFactory } from "./login.strategy.spec"; import { SsoLoginStrategy } from "./sso-login.strategy"; describe("SsoLoginStrategy", () => { + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; + let cryptoService: MockProxy; let apiService: MockProxy; let tokenService: MockProxy; @@ -52,6 +58,7 @@ describe("SsoLoginStrategy", () => { let ssoLoginStrategy: SsoLoginStrategy; let credentials: SsoLoginCredentials; + const userId = Utils.newGuid() as UserId; const deviceId = Utils.newGuid(); const keyConnectorUrl = "KEY_CONNECTOR_URL"; @@ -61,6 +68,9 @@ describe("SsoLoginStrategy", () => { const ssoOrgId = "SSO_ORG_ID"; beforeEach(async () => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); + cryptoService = mock(); apiService = mock(); tokenService = mock(); @@ -83,6 +93,8 @@ describe("SsoLoginStrategy", () => { ssoLoginStrategy = new SsoLoginStrategy( null, + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -130,7 +142,7 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); - expect(cryptoService.setMasterKey).not.toHaveBeenCalled(); + expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled(); expect(cryptoService.setUserKey).not.toHaveBeenCalled(); expect(cryptoService.setPrivateKey).not.toHaveBeenCalled(); }); @@ -289,7 +301,7 @@ describe("SsoLoginStrategy", () => { id: "1", privateKey: "PRIVATE" as any, } as AdminAuthRequestStorable; - stateService.getAdminAuthRequest.mockResolvedValue( + authRequestService.getAdminAuthRequest.mockResolvedValue( new AdminAuthRequestStorable(adminAuthRequest), ); }); @@ -352,7 +364,7 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); - expect(stateService.setAdminAuthRequest).toHaveBeenCalledWith(null); + expect(authRequestService.clearAdminAuthRequest).toHaveBeenCalled(); expect( authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash, ).not.toHaveBeenCalled(); @@ -395,7 +407,7 @@ describe("SsoLoginStrategy", () => { ) as MasterKey; apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); await ssoLoginStrategy.logIn(credentials); @@ -422,7 +434,7 @@ describe("SsoLoginStrategy", () => { ) as MasterKey; apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await ssoLoginStrategy.logIn(credentials); @@ -446,7 +458,7 @@ describe("SsoLoginStrategy", () => { ) as MasterKey; apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); await ssoLoginStrategy.logIn(credentials); @@ -473,7 +485,7 @@ describe("SsoLoginStrategy", () => { ) as MasterKey; apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await ssoLoginStrategy.logIn(credentials); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index 7745104bd1..d8efd78984 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -1,9 +1,11 @@ -import { Observable, map, BehaviorSubject } from "rxjs"; +import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -79,6 +81,8 @@ export class SsoLoginStrategy extends LoginStrategy { constructor( data: SsoLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -96,6 +100,8 @@ export class SsoLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -138,7 +144,11 @@ export class SsoLoginStrategy extends LoginStrategy { // Auth guard currently handles redirects for this. if (ssoAuthResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) { - await this.stateService.setForceSetPasswordReason(ssoAuthResult.forcePasswordReset); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( + ssoAuthResult.forcePasswordReset, + userId, + ); } this.cache.next({ @@ -206,7 +216,10 @@ export class SsoLoginStrategy extends LoginStrategy { // TODO: future passkey login strategy will need to support setting user key (decrypting via TDE or admin approval request) // so might be worth moving this logic to a common place (base login strategy or a separate service?) - protected override async setUserKey(tokenResponse: IdentityTokenResponse): Promise { + protected override async setUserKey( + tokenResponse: IdentityTokenResponse, + userId: UserId, + ): Promise { const masterKeyEncryptedUserKey = tokenResponse.key; // Note: masterKeyEncryptedUserKey is undefined for SSO JIT provisioned users @@ -222,7 +235,7 @@ export class SsoLoginStrategy extends LoginStrategy { // Note: TDE and key connector are mutually exclusive if (userDecryptionOptions?.trustedDeviceOption) { - await this.trySetUserKeyWithApprovedAdminRequestIfExists(); + await this.trySetUserKeyWithApprovedAdminRequestIfExists(userId); const hasUserKey = await this.cryptoService.hasUserKey(); @@ -242,9 +255,9 @@ export class SsoLoginStrategy extends LoginStrategy { // is responsible for deriving master key from MP entry and then decrypting the user key } - private async trySetUserKeyWithApprovedAdminRequestIfExists(): Promise { + private async trySetUserKeyWithApprovedAdminRequestIfExists(userId: UserId): Promise { // At this point a user could have an admin auth request that has been approved - const adminAuthReqStorable = await this.stateService.getAdminAuthRequest(); + const adminAuthReqStorable = await this.authRequestService.getAdminAuthRequest(userId); if (!adminAuthReqStorable) { return; @@ -258,7 +271,7 @@ export class SsoLoginStrategy extends LoginStrategy { } catch (error) { if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) { // if we get a 404, it means the auth request has been deleted so clear it from storage - await this.stateService.setAdminAuthRequest(null); + await this.authRequestService.clearAdminAuthRequest(userId); } // Always return on an error here as we don't want to block the user from logging in @@ -285,12 +298,11 @@ export class SsoLoginStrategy extends LoginStrategy { if (await this.cryptoService.hasUserKey()) { // Now that we have a decrypted user key in memory, we can check if we // need to establish trust on the current device - const userId = (await this.stateService.getUserId()) as UserId; await this.deviceTrustCryptoService.trustDeviceIfRequired(userId); // if we successfully decrypted the user key, we can delete the admin auth request out of state // TODO: eventually we post and clean up DB as well once consumed on client - await this.stateService.setAdminAuthRequest(null); + await this.authRequestService.clearAdminAuthRequest(userId); this.platformUtilsService.showToast("success", null, this.i18nService.t("loginApproved")); } @@ -323,7 +335,8 @@ export class SsoLoginStrategy extends LoginStrategy { } private async trySetUserKeyWithMasterKey(): Promise { - const masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); // There is a scenario in which the master key is not set here. That will occur if the user // has a master password and is using Key Connector. In that case, we cannot set the master key diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts index 02aed305a4..5e7d7985b1 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts @@ -5,6 +5,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; @@ -19,7 +20,9 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { UserId } from "@bitwarden/common/types/guid"; import { UserKey, MasterKey } from "@bitwarden/common/types/key"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -30,6 +33,8 @@ import { UserApiLoginStrategy, UserApiLoginStrategyData } from "./user-api-login describe("UserApiLoginStrategy", () => { let cache: UserApiLoginStrategyData; + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; let cryptoService: MockProxy; let apiService: MockProxy; @@ -48,12 +53,16 @@ describe("UserApiLoginStrategy", () => { let apiLogInStrategy: UserApiLoginStrategy; let credentials: UserApiLoginCredentials; + const userId = Utils.newGuid() as UserId; const deviceId = Utils.newGuid(); const keyConnectorUrl = "KEY_CONNECTOR_URL"; const apiClientId = "API_CLIENT_ID"; const apiClientSecret = "API_CLIENT_SECRET"; beforeEach(async () => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); + cryptoService = mock(); apiService = mock(); tokenService = mock(); @@ -74,6 +83,8 @@ describe("UserApiLoginStrategy", () => { apiLogInStrategy = new UserApiLoginStrategy( cache, + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -172,7 +183,7 @@ describe("UserApiLoginStrategy", () => { environmentService.environment$ = new BehaviorSubject(env); apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await apiLogInStrategy.logIn(credentials); diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts index 2af666f95c..4a0d005b1c 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts @@ -2,7 +2,9 @@ import { firstValueFrom, BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { UserApiTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/user-api-token.request"; @@ -16,6 +18,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { UserApiLoginCredentials } from "../models/domain/login-credentials"; @@ -39,6 +42,8 @@ export class UserApiLoginStrategy extends LoginStrategy { constructor( data: UserApiLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -54,6 +59,8 @@ export class UserApiLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -91,11 +98,15 @@ export class UserApiLoginStrategy extends LoginStrategy { } } - protected override async setUserKey(response: IdentityTokenResponse): Promise { + protected override async setUserKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise { await this.cryptoService.setMasterKeyEncryptedUserKey(response.key); if (response.apiUseKeyConnector) { - const masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (masterKey) { const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); await this.cryptoService.setUserKey(userKey); @@ -109,8 +120,8 @@ export class UserApiLoginStrategy extends LoginStrategy { ); } - protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) { - await super.saveAccountInformation(tokenResponse); + protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise { + const userId = await super.saveAccountInformation(tokenResponse); const vaultTimeout = await this.stateService.getVaultTimeout(); const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction(); @@ -127,6 +138,7 @@ export class UserApiLoginStrategy extends LoginStrategy { vaultTimeoutAction as VaultTimeoutAction, vaultTimeout, ); + return userId; } exportCache(): CacheData { diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts index edc1441361..1d96921286 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts @@ -6,6 +6,7 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; @@ -16,6 +17,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { FakeAccountService } from "@bitwarden/common/spec"; import { PrfKey, UserKey } from "@bitwarden/common/types/key"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -26,6 +28,8 @@ import { WebAuthnLoginStrategy, WebAuthnLoginStrategyData } from "./webauthn-log describe("WebAuthnLoginStrategy", () => { let cache: WebAuthnLoginStrategyData; + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; let cryptoService!: MockProxy; let apiService!: MockProxy; @@ -63,6 +67,9 @@ describe("WebAuthnLoginStrategy", () => { beforeEach(() => { jest.clearAllMocks(); + accountService = new FakeAccountService(null); + masterPasswordService = new FakeMasterPasswordService(); + cryptoService = mock(); apiService = mock(); tokenService = mock(); @@ -81,6 +88,8 @@ describe("WebAuthnLoginStrategy", () => { webAuthnLoginStrategy = new WebAuthnLoginStrategy( cache, + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -207,7 +216,7 @@ describe("WebAuthnLoginStrategy", () => { expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey); // Master key and private key should not be set - expect(cryptoService.setMasterKey).not.toHaveBeenCalled(); + expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled(); }); it("does not try to set the user key when prfKey is missing", async () => { diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts index a8e67597b8..8a62a8fb3c 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts @@ -2,6 +2,8 @@ import { BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; @@ -15,6 +17,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions"; @@ -41,6 +44,8 @@ export class WebAuthnLoginStrategy extends LoginStrategy { constructor( data: WebAuthnLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -54,6 +59,8 @@ export class WebAuthnLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -92,7 +99,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { return Promise.resolve(); } - protected override async setUserKey(idTokenResponse: IdentityTokenResponse) { + protected override async setUserKey(idTokenResponse: IdentityTokenResponse, userId: UserId) { const masterKeyEncryptedUserKey = idTokenResponse.key; if (masterKeyEncryptedUserKey) { diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts index 80d00b2a01..5907048684 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts @@ -2,13 +2,16 @@ import { mock } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { AuthRequestService } from "./auth-request.service"; @@ -16,17 +19,29 @@ import { AuthRequestService } from "./auth-request.service"; describe("AuthRequestService", () => { let sut: AuthRequestService; + const stateProvider = mock(); + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; const appIdService = mock(); const cryptoService = mock(); const apiService = mock(); - const stateService = mock(); let mockPrivateKey: Uint8Array; + const mockUserId = Utils.newGuid() as UserId; beforeEach(() => { jest.clearAllMocks(); + accountService = mockAccountServiceWith(mockUserId); + masterPasswordService = new FakeMasterPasswordService(); - sut = new AuthRequestService(appIdService, cryptoService, apiService, stateService); + sut = new AuthRequestService( + appIdService, + accountService, + masterPasswordService, + cryptoService, + apiService, + stateProvider, + ); mockPrivateKey = new Uint8Array(64); }); @@ -47,6 +62,31 @@ describe("AuthRequestService", () => { }); }); + describe("AcceptAuthRequests", () => { + it("returns an error when userId isn't provided", async () => { + await expect(sut.getAcceptAuthRequests(undefined)).rejects.toThrow("User ID is required"); + await expect(sut.setAcceptAuthRequests(true, undefined)).rejects.toThrow( + "User ID is required", + ); + }); + }); + + describe("AdminAuthRequest", () => { + it("returns an error when userId isn't provided", async () => { + await expect(sut.getAdminAuthRequest(undefined)).rejects.toThrow("User ID is required"); + await expect(sut.setAdminAuthRequest(undefined, undefined)).rejects.toThrow( + "User ID is required", + ); + await expect(sut.clearAdminAuthRequest(undefined)).rejects.toThrow("User ID is required"); + }); + + it("does not allow clearing from setAdminAuthRequest", async () => { + await expect(sut.setAdminAuthRequest(null, "USER_ID" as UserId)).rejects.toThrow( + "Auth request is required", + ); + }); + }); + describe("approveOrDenyAuthRequest", () => { beforeEach(() => { cryptoService.rsaEncrypt.mockResolvedValue({ @@ -67,8 +107,8 @@ describe("AuthRequestService", () => { }); it("should use the master key and hash if they exist", async () => { - cryptoService.getMasterKey.mockResolvedValueOnce({ encKey: new Uint8Array(64) } as MasterKey); - stateService.getKeyHash.mockResolvedValueOnce("KEY_HASH"); + masterPasswordService.masterKeySubject.next({ encKey: new Uint8Array(64) } as MasterKey); + masterPasswordService.masterKeyHashSubject.next("MASTER_KEY_HASH"); await sut.approveOrDenyAuthRequest( true, @@ -130,8 +170,8 @@ describe("AuthRequestService", () => { masterKeyHash: mockDecryptedMasterKeyHash, }); - cryptoService.setMasterKey.mockResolvedValueOnce(undefined); - cryptoService.setMasterKeyHash.mockResolvedValueOnce(undefined); + masterPasswordService.masterKeySubject.next(undefined); + masterPasswordService.masterKeyHashSubject.next(undefined); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValueOnce(mockDecryptedUserKey); cryptoService.setUserKey.mockResolvedValueOnce(undefined); @@ -144,10 +184,18 @@ describe("AuthRequestService", () => { mockAuthReqResponse.masterPasswordHash, mockPrivateKey, ); - expect(cryptoService.setMasterKey).toBeCalledWith(mockDecryptedMasterKey); - expect(cryptoService.setMasterKeyHash).toBeCalledWith(mockDecryptedMasterKeyHash); - expect(cryptoService.decryptUserKeyWithMasterKey).toBeCalledWith(mockDecryptedMasterKey); - expect(cryptoService.setUserKey).toBeCalledWith(mockDecryptedUserKey); + expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith( + mockDecryptedMasterKey, + mockUserId, + ); + expect(masterPasswordService.mock.setMasterKeyHash).toHaveBeenCalledWith( + mockDecryptedMasterKeyHash, + mockUserId, + ); + expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith( + mockDecryptedMasterKey, + ); + expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockDecryptedUserKey); }); }); diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.ts b/libs/auth/src/common/services/auth-request/auth-request.service.ts index eb39659f53..062a10af14 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.ts @@ -1,31 +1,119 @@ -import { Observable, Subject } from "rxjs"; +import { Observable, Subject, firstValueFrom } from "rxjs"; +import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; +import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable"; import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { + AUTH_REQUEST_DISK_LOCAL, + StateProvider, + UserKeyDefinition, +} from "@bitwarden/common/platform/state"; +import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { AuthRequestServiceAbstraction } from "../../abstractions/auth-request.service.abstraction"; +/** + * Disk-local to maintain consistency between tabs (even though + * approvals are currently only available on desktop). We don't + * want to clear this on logout as it's a user preference. + */ +export const ACCEPT_AUTH_REQUESTS_KEY = new UserKeyDefinition( + AUTH_REQUEST_DISK_LOCAL, + "acceptAuthRequests", + { + deserializer: (value) => value ?? false, + clearOn: [], + }, +); + +/** + * Disk-local to maintain consistency between tabs. We don't want to + * clear this on logout since admin auth requests are long-lived. + */ +export const ADMIN_AUTH_REQUEST_KEY = new UserKeyDefinition>( + AUTH_REQUEST_DISK_LOCAL, + "adminAuthRequest", + { + deserializer: (value) => value, + clearOn: [], + }, +); + export class AuthRequestService implements AuthRequestServiceAbstraction { private authRequestPushNotificationSubject = new Subject(); authRequestPushNotification$: Observable; constructor( private appIdService: AppIdService, + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private apiService: ApiService, - private stateService: StateService, + private stateProvider: StateProvider, ) { this.authRequestPushNotification$ = this.authRequestPushNotificationSubject.asObservable(); } + async getAcceptAuthRequests(userId: UserId): Promise { + if (userId == null) { + throw new Error("User ID is required"); + } + + const value = await firstValueFrom( + this.stateProvider.getUser(userId, ACCEPT_AUTH_REQUESTS_KEY).state$, + ); + return value; + } + + async setAcceptAuthRequests(accept: boolean, userId: UserId): Promise { + if (userId == null) { + throw new Error("User ID is required"); + } + + await this.stateProvider.setUserState(ACCEPT_AUTH_REQUESTS_KEY, accept, userId); + } + + async getAdminAuthRequest(userId: UserId): Promise { + if (userId == null) { + throw new Error("User ID is required"); + } + + const authRequestSerialized = await firstValueFrom( + this.stateProvider.getUser(userId, ADMIN_AUTH_REQUEST_KEY).state$, + ); + const adminAuthRequestStorable = AdminAuthRequestStorable.fromJSON(authRequestSerialized); + return adminAuthRequestStorable; + } + + async setAdminAuthRequest(authRequest: AdminAuthRequestStorable, userId: UserId): Promise { + if (userId == null) { + throw new Error("User ID is required"); + } + if (authRequest == null) { + throw new Error("Auth request is required"); + } + + await this.stateProvider.setUserState(ADMIN_AUTH_REQUEST_KEY, authRequest.toJSON(), userId); + } + + async clearAdminAuthRequest(userId: UserId): Promise { + if (userId == null) { + throw new Error("User ID is required"); + } + + await this.stateProvider.setUserState(ADMIN_AUTH_REQUEST_KEY, null, userId); + } + async approveOrDenyAuthRequest( approve: boolean, authRequest: AuthRequestResponse, @@ -38,8 +126,9 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { } const pubKey = Utils.fromB64ToArray(authRequest.publicKey); - const masterKey = await this.cryptoService.getMasterKey(); - const masterKeyHash = await this.stateService.getKeyHash(); + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + const masterKeyHash = await firstValueFrom(this.masterPasswordService.masterKeyHash$(userId)); let encryptedMasterKeyHash; let keyToEncrypt; @@ -92,8 +181,9 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); // Set masterKey + masterKeyHash in state after decryption (in case decryption fails) - await this.cryptoService.setMasterKey(masterKey); - await this.cryptoService.setMasterKeyHash(masterKeyHash); + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + await this.masterPasswordService.setMasterKey(masterKey, userId); + await this.masterPasswordService.setMasterKeyHash(masterKeyHash, userId); await this.cryptoService.setUserKey(userKey); } diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts index 981e4d81ac..fcc0220d0a 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts @@ -11,6 +11,7 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -22,8 +23,14 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { KdfType } from "@bitwarden/common/platform/enums"; -import { FakeGlobalState, FakeGlobalStateProvider } from "@bitwarden/common/spec"; +import { + FakeAccountService, + FakeGlobalState, + FakeGlobalStateProvider, + mockAccountServiceWith, +} from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; import { AuthRequestServiceAbstraction, @@ -38,6 +45,8 @@ import { CACHE_EXPIRATION_KEY } from "./login-strategy.state"; describe("LoginStrategyService", () => { let sut: LoginStrategyService; + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; let cryptoService: MockProxy; let apiService: MockProxy; let tokenService: MockProxy; @@ -61,7 +70,11 @@ describe("LoginStrategyService", () => { let stateProvider: FakeGlobalStateProvider; let loginStrategyCacheExpirationState: FakeGlobalState; + const userId = "USER_ID" as UserId; + beforeEach(() => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); cryptoService = mock(); apiService = mock(); tokenService = mock(); @@ -84,6 +97,8 @@ describe("LoginStrategyService", () => { stateProvider = new FakeGlobalStateProvider(); sut = new LoginStrategyService( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index b55f38af7f..a8bd7bc2ff 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -9,8 +9,10 @@ import { import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; @@ -81,6 +83,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { currentAuthType$: Observable; constructor( + protected accountService: AccountService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected cryptoService: CryptoService, protected apiService: ApiService, protected tokenService: TokenService, @@ -257,7 +261,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { ): Promise { const pubKey = Utils.fromB64ToArray(key); - const masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); let keyToEncrypt; let encryptedMasterKeyHash = null; @@ -266,7 +271,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { // Only encrypt the master password hash if masterKey exists as // we won't have a masterKeyHash without a masterKey - const masterKeyHash = await this.stateService.getKeyHash(); + const masterKeyHash = await firstValueFrom(this.masterPasswordService.masterKeyHash$(userId)); if (masterKeyHash != null) { encryptedMasterKeyHash = await this.cryptoService.rsaEncrypt( Utils.fromUtf8ToArray(masterKeyHash), @@ -333,6 +338,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { case AuthenticationType.Password: return new PasswordLoginStrategy( data?.password, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -351,6 +358,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { case AuthenticationType.Sso: return new SsoLoginStrategy( data?.sso, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -370,6 +379,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { case AuthenticationType.UserApiKey: return new UserApiLoginStrategy( data?.userApiKey, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -387,6 +398,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { case AuthenticationType.AuthRequest: return new AuthRequestLoginStrategy( data?.authRequest, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -403,6 +416,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { case AuthenticationType.WebAuthn: return new WebAuthnLoginStrategy( data?.webAuthn, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, diff --git a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts index e8bb1b38ce..16479f19ea 100644 --- a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts +++ b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts @@ -1,6 +1,5 @@ import { firstValueFrom } from "rxjs"; -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { FakeAccountService, @@ -66,7 +65,6 @@ describe("UserDecryptionOptionsService", () => { await fakeAccountService.addAccount(givenUser, { name: "Test User 1", email: "test1@email.com", - status: AuthenticationStatus.Locked, }); await fakeStateProvider.setUserState( USER_DECRYPTION_OPTIONS, diff --git a/libs/auth/src/icons/bitwarden-logo.ts b/libs/auth/src/icons/bitwarden-logo.ts index 90591e0fe7..872228e75d 100644 --- a/libs/auth/src/icons/bitwarden-logo.ts +++ b/libs/auth/src/icons/bitwarden-logo.ts @@ -3,7 +3,7 @@ import { svgIcon } from "@bitwarden/components"; export const BitwardenLogo = svgIcon` Bitwarden - - + + `; diff --git a/libs/auth/src/icons/icon-lock.ts b/libs/auth/src/icons/icon-lock.ts index b56c1ea36d..61330fe0df 100644 --- a/libs/auth/src/icons/icon-lock.ts +++ b/libs/auth/src/icons/icon-lock.ts @@ -2,6 +2,6 @@ import { svgIcon } from "@bitwarden/components"; export const IconLock = svgIcon` - + `; diff --git a/libs/common/spec/fake-account-service.ts b/libs/common/spec/fake-account-service.ts index 2f33d9cf02..a8b09b7417 100644 --- a/libs/common/spec/fake-account-service.ts +++ b/libs/common/spec/fake-account-service.ts @@ -1,8 +1,7 @@ import { mock } from "jest-mock-extended"; -import { Observable, ReplaySubject } from "rxjs"; +import { ReplaySubject } from "rxjs"; import { AccountInfo, AccountService } from "../src/auth/abstractions/account.service"; -import { AuthenticationStatus } from "../src/auth/enums/authentication-status"; import { UserId } from "../src/types/guid"; export function mockAccountServiceWith( @@ -14,7 +13,6 @@ export function mockAccountServiceWith( ...{ name: "name", email: "email", - status: AuthenticationStatus.Locked, }, }; const service = new FakeAccountService({ [userId]: fullInfo }); @@ -32,14 +30,8 @@ export class FakeAccountService implements AccountService { get activeUserId() { return this._activeUserId; } - get accounts$() { - return this.accountsSubject.asObservable(); - } - get activeAccount$() { - return this.activeAccountSubject.asObservable(); - } - accountLock$: Observable; - accountLogout$: Observable; + accounts$ = this.accountsSubject.asObservable(); + activeAccount$ = this.activeAccountSubject.asObservable(); constructor(initialData: Record) { this.accountsSubject.next(initialData); @@ -61,14 +53,6 @@ export class FakeAccountService implements AccountService { await this.mock.setAccountEmail(userId, email); } - async setAccountStatus(userId: UserId, status: AuthenticationStatus): Promise { - await this.mock.setAccountStatus(userId, status); - } - - async setMaxAccountStatus(userId: UserId, maxStatus: AuthenticationStatus): Promise { - await this.mock.setMaxAccountStatus(userId, maxStatus); - } - async switchAccount(userId: UserId): Promise { const next = userId == null ? null : { id: userId, ...this.accountsSubject["_buffer"]?.[0]?.[userId] }; diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 20ed3216a5..9b3160ee19 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -4,8 +4,6 @@ import { OrganizationSponsorshipRedeemRequest } from "../admin-console/models/re import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request"; import { ProviderAddOrganizationRequest } from "../admin-console/models/request/provider/provider-add-organization.request"; import { ProviderOrganizationCreateRequest } from "../admin-console/models/request/provider/provider-organization-create.request"; -import { ProviderSetupRequest } from "../admin-console/models/request/provider/provider-setup.request"; -import { ProviderUpdateRequest } from "../admin-console/models/request/provider/provider-update.request"; import { ProviderUserAcceptRequest } from "../admin-console/models/request/provider/provider-user-accept.request"; import { ProviderUserBulkConfirmRequest } from "../admin-console/models/request/provider/provider-user-bulk-confirm.request"; import { ProviderUserBulkRequest } from "../admin-console/models/request/provider/provider-user-bulk.request"; @@ -29,7 +27,6 @@ import { ProviderUserResponse, ProviderUserUserDetailsResponse, } from "../admin-console/models/response/provider/provider-user.response"; -import { ProviderResponse } from "../admin-console/models/response/provider/provider.response"; import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response"; import { CreateAuthRequest } from "../auth/models/request/create-auth.request"; import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request"; @@ -220,7 +217,7 @@ export abstract class ApiService { putMoveCiphers: (request: CipherBulkMoveRequest) => Promise; putShareCipher: (id: string, request: CipherShareRequest) => Promise; putShareCiphers: (request: CipherBulkShareRequest) => Promise; - putCipherCollections: (id: string, request: CipherCollectionsRequest) => Promise; + putCipherCollections: (id: string, request: CipherCollectionsRequest) => Promise; putCipherCollectionsAdmin: (id: string, request: CipherCollectionsRequest) => Promise; postPurgeCiphers: (request: SecretVerificationRequest, organizationId?: string) => Promise; putDeleteCipher: (id: string) => Promise; @@ -297,7 +294,6 @@ export abstract class ApiService { ) => Promise; getGroupUsers: (organizationId: string, id: string) => Promise; - putGroupUsers: (organizationId: string, id: string, request: string[]) => Promise; deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise; getSync: () => Promise; @@ -373,10 +369,6 @@ export abstract class ApiService { getPlans: () => Promise>; getTaxRates: () => Promise>; - postProviderSetup: (id: string, request: ProviderSetupRequest) => Promise; - getProvider: (id: string) => Promise; - putProvider: (id: string, request: ProviderUpdateRequest) => Promise; - getProviderUsers: (providerId: string) => Promise>; getProviderUser: (providerId: string, id: string) => Promise; postProviderUserInvite: (providerId: string, request: ProviderUserInviteRequest) => Promise; diff --git a/libs/common/src/abstractions/search.service.ts b/libs/common/src/abstractions/search.service.ts index 97a12c8315..dfcf2c5d07 100644 --- a/libs/common/src/abstractions/search.service.ts +++ b/libs/common/src/abstractions/search.service.ts @@ -1,11 +1,15 @@ +import { Observable } from "rxjs"; + import { SendView } from "../tools/send/models/view/send.view"; +import { IndexedEntityId } from "../types/guid"; import { CipherView } from "../vault/models/view/cipher.view"; export abstract class SearchService { - indexedEntityId?: string = null; - clearIndex: () => void; - isSearchable: (query: string) => boolean; - indexCiphers: (ciphersToIndex: CipherView[], indexedEntityGuid?: string) => void; + indexedEntityId$: Observable; + + clearIndex: () => Promise; + isSearchable: (query: string) => Promise; + indexCiphers: (ciphersToIndex: CipherView[], indexedEntityGuid?: string) => Promise; searchCiphers: ( query: string, filter?: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[], diff --git a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts index 7f1a40d140..66a05cf613 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts @@ -3,9 +3,9 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request"; import { ApiKeyResponse } from "../../../auth/models/response/api-key.response"; import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response"; +import { ExpandedTaxInfoUpdateRequest } from "../../../billing/models/request/expanded-tax-info-update.request"; import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request"; import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request"; -import { OrganizationTaxInfoUpdateRequest } from "../../../billing/models/request/organization-tax-info-update.request"; import { PaymentRequest } from "../../../billing/models/request/payment.request"; import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request"; import { BillingResponse } from "../../../billing/models/response/billing.response"; @@ -63,7 +63,7 @@ export class OrganizationApiServiceAbstraction { ) => Promise>; rotateApiKey: (id: string, request: OrganizationApiKeyRequest) => Promise; getTaxInfo: (id: string) => Promise; - updateTaxInfo: (id: string, request: OrganizationTaxInfoUpdateRequest) => Promise; + updateTaxInfo: (id: string, request: ExpandedTaxInfoUpdateRequest) => Promise; getKeys: (id: string) => Promise; updateKeys: (id: string, request: OrganizationKeysRequest) => Promise; getSso: (id: string) => Promise; diff --git a/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts index a1ae64a885..fefcac3a57 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts @@ -121,7 +121,11 @@ export abstract class OrganizationService { get$: (id: string) => Observable; get: (id: string) => Promise; getAll: (userId?: string) => Promise; - // + + /** + * Publishes state for all organizations for the given user id or the active user. + */ + getAll$: (userId?: UserId) => Observable; } /** diff --git a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts index fb805f94cd..21669f78ad 100644 --- a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts @@ -78,5 +78,4 @@ export abstract class PolicyService { export abstract class InternalPolicyService extends PolicyService { upsert: (policy: PolicyData) => Promise; replace: (policies: { [id: string]: PolicyData }) => Promise; - clear: (userId?: string) => Promise; } diff --git a/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts new file mode 100644 index 0000000000..3c2170bf9e --- /dev/null +++ b/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts @@ -0,0 +1,15 @@ +import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request"; +import { ProviderUpdateRequest } from "../../models/request/provider/provider-update.request"; +import { ProviderVerifyRecoverDeleteRequest } from "../../models/request/provider/provider-verify-recover-delete.request"; +import { ProviderResponse } from "../../models/response/provider/provider.response"; + +export class ProviderApiServiceAbstraction { + postProviderSetup: (id: string, request: ProviderSetupRequest) => Promise; + getProvider: (id: string) => Promise; + putProvider: (id: string, request: ProviderUpdateRequest) => Promise; + providerRecoverDeleteToken: ( + organizationId: string, + request: ProviderVerifyRecoverDeleteRequest, + ) => Promise; + deleteProvider: (id: string) => Promise; +} diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index 5850f4582e..bdf0b8fbbf 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -203,8 +203,9 @@ export class Organization { ); } - canUseAdminCollections(flexibleCollectionsV1Enabled: boolean) { - return this.canEditAnyCollection(flexibleCollectionsV1Enabled); + canEditUnassignedCiphers() { + // TODO: Update this to exclude Providers if provider access is restricted in AC-1707 + return this.isAdmin || this.permissions.editAnyCollection; } canEditAllCiphers(flexibleCollectionsV1Enabled: boolean) { diff --git a/libs/common/src/admin-console/models/request/provider/provider-setup.request.ts b/libs/common/src/admin-console/models/request/provider/provider-setup.request.ts index 61eb943f1d..7dc664869c 100644 --- a/libs/common/src/admin-console/models/request/provider/provider-setup.request.ts +++ b/libs/common/src/admin-console/models/request/provider/provider-setup.request.ts @@ -1,7 +1,10 @@ +import { ExpandedTaxInfoUpdateRequest } from "../../../../billing/models/request/expanded-tax-info-update.request"; + export class ProviderSetupRequest { name: string; businessName: string; billingEmail: string; token: string; key: string; + taxInfo: ExpandedTaxInfoUpdateRequest; } diff --git a/libs/common/src/admin-console/models/request/provider/provider-verify-recover-delete.request.ts b/libs/common/src/admin-console/models/request/provider/provider-verify-recover-delete.request.ts new file mode 100644 index 0000000000..528d2dba78 --- /dev/null +++ b/libs/common/src/admin-console/models/request/provider/provider-verify-recover-delete.request.ts @@ -0,0 +1,7 @@ +export class ProviderVerifyRecoverDeleteRequest { + token: string; + + constructor(token: string) { + this.token = token; + } +} diff --git a/libs/common/src/admin-console/services/organization-domain/requests/organization-domain.request.ts b/libs/common/src/admin-console/services/organization-domain/requests/organization-domain.request.ts index fb515e3cbc..c316a1d27c 100644 --- a/libs/common/src/admin-console/services/organization-domain/requests/organization-domain.request.ts +++ b/libs/common/src/admin-console/services/organization-domain/requests/organization-domain.request.ts @@ -1,9 +1,7 @@ export class OrganizationDomainRequest { - txt: string; domainName: string; - constructor(txt: string, domainName: string) { - this.txt = txt; + constructor(domainName: string) { this.domainName = domainName; } } diff --git a/libs/common/src/admin-console/services/organization/organization-api.service.ts b/libs/common/src/admin-console/services/organization/organization-api.service.ts index 883bf35260..262232a964 100644 --- a/libs/common/src/admin-console/services/organization/organization-api.service.ts +++ b/libs/common/src/admin-console/services/organization/organization-api.service.ts @@ -4,9 +4,9 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request"; import { ApiKeyResponse } from "../../../auth/models/response/api-key.response"; import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response"; +import { ExpandedTaxInfoUpdateRequest } from "../../../billing/models/request/expanded-tax-info-update.request"; import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request"; import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request"; -import { OrganizationTaxInfoUpdateRequest } from "../../../billing/models/request/organization-tax-info-update.request"; import { PaymentRequest } from "../../../billing/models/request/payment.request"; import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request"; import { BillingResponse } from "../../../billing/models/response/billing.response"; @@ -257,7 +257,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction return new TaxInfoResponse(r); } - async updateTaxInfo(id: string, request: OrganizationTaxInfoUpdateRequest): Promise { + async updateTaxInfo(id: string, request: ExpandedTaxInfoUpdateRequest): Promise { // Can't broadcast anything because the response doesn't have content return this.apiService.send("PUT", "/organizations/" + id + "/tax", request, true, false); } diff --git a/libs/common/src/admin-console/services/organization/organization.service.ts b/libs/common/src/admin-console/services/organization/organization.service.ts index 411850fe30..7013863c5c 100644 --- a/libs/common/src/admin-console/services/organization/organization.service.ts +++ b/libs/common/src/admin-console/services/organization/organization.service.ts @@ -73,6 +73,10 @@ export class OrganizationService implements InternalOrganizationServiceAbstracti return this.organizations$.pipe(mapToSingleOrganization(id)); } + getAll$(userId?: UserId): Observable { + return this.getOrganizationsFromState$(userId); + } + async getAll(userId?: string): Promise { return await firstValueFrom(this.getOrganizationsFromState$(userId as UserId)); } diff --git a/libs/common/src/admin-console/services/policy/policy.service.spec.ts b/libs/common/src/admin-console/services/policy/policy.service.spec.ts index 8fa79f4d1c..88264d1c3b 100644 --- a/libs/common/src/admin-console/services/policy/policy.service.spec.ts +++ b/libs/common/src/admin-console/services/policy/policy.service.spec.ts @@ -32,7 +32,8 @@ describe("PolicyService", () => { organizationService = mock(); activeUserState = stateProvider.activeUser.getFake(POLICIES); - organizationService.organizations$ = of([ + + const organizations$ = of([ // User organization("org1", true, true, OrganizationUserStatusType.Confirmed, false), // Owner @@ -50,8 +51,14 @@ describe("PolicyService", () => { organization("org4", true, true, OrganizationUserStatusType.Confirmed, false), // Another User organization("org5", true, true, OrganizationUserStatusType.Confirmed, false), + // Can manage policies + organization("org6", true, true, OrganizationUserStatusType.Confirmed, true), ]); + organizationService.organizations$ = organizations$; + + organizationService.getAll$.mockReturnValue(organizations$); + policyService = new PolicyService(stateProvider, organizationService); }); @@ -102,66 +109,6 @@ describe("PolicyService", () => { ]); }); - describe("clear", () => { - beforeEach(() => { - activeUserState.nextState( - arrayToRecord([ - policyData("1", "test-organization", PolicyType.MaximumVaultTimeout, true, { - minutes: 14, - }), - ]), - ); - }); - - it("clears state for the active user", async () => { - await policyService.clear(); - - expect(await firstValueFrom(policyService.policies$)).toEqual([]); - expect(await firstValueFrom(activeUserState.state$)).toEqual(null); - expect(stateProvider.activeUser.getFake(POLICIES).nextMock).toHaveBeenCalledWith([ - "userId", - null, - ]); - }); - - it("clears state for an inactive user", async () => { - const inactiveUserId = "someOtherUserId" as UserId; - const inactiveUserState = stateProvider.singleUser.getFake(inactiveUserId, POLICIES); - inactiveUserState.nextState( - arrayToRecord([ - policyData("10", "another-test-organization", PolicyType.PersonalOwnership, true), - ]), - ); - - await policyService.clear(inactiveUserId); - - // Active user is not affected - const expectedActiveUserPolicy: Partial = { - id: "1" as PolicyId, - organizationId: "test-organization", - type: PolicyType.MaximumVaultTimeout, - enabled: true, - data: { minutes: 14 }, - }; - expect(await firstValueFrom(policyService.policies$)).toEqual([expectedActiveUserPolicy]); - expect(await firstValueFrom(activeUserState.state$)).toEqual({ - "1": expectedActiveUserPolicy, - }); - expect(stateProvider.activeUser.getFake(POLICIES).nextMock).not.toHaveBeenCalled(); - - // Non-active user is cleared - expect( - await firstValueFrom( - policyService.getAll$(PolicyType.PersonalOwnership, "someOtherUserId" as UserId), - ), - ).toEqual([]); - expect(await firstValueFrom(inactiveUserState.state$)).toEqual(null); - expect( - stateProvider.singleUser.getFake("someOtherUserId" as UserId, POLICIES).nextMock, - ).toHaveBeenCalledWith(null); - }); - }); - describe("masterPasswordPolicyOptions", () => { it("returns default policy options", async () => { const data: any = { @@ -314,6 +261,22 @@ describe("PolicyService", () => { expect(result).toBeNull(); }); + it.each([ + ["owners", "org2"], + ["administrators", "org6"], + ])("returns the password generator policy for %s", async (_, organization) => { + activeUserState.nextState( + arrayToRecord([ + policyData("policy1", "org1", PolicyType.ActivateAutofill, false), + policyData("policy2", organization, PolicyType.PasswordGenerator, true), + ]), + ); + + const result = await firstValueFrom(policyService.get$(PolicyType.PasswordGenerator)); + + expect(result).toBeTruthy(); + }); + it("does not return policies for organizations that do not use policies", async () => { activeUserState.nextState( arrayToRecord([ diff --git a/libs/common/src/admin-console/services/policy/policy.service.ts b/libs/common/src/admin-console/services/policy/policy.service.ts index d60d2e3293..e36902cbf9 100644 --- a/libs/common/src/admin-console/services/policy/policy.service.ts +++ b/libs/common/src/admin-console/services/policy/policy.service.ts @@ -1,6 +1,6 @@ import { combineLatest, firstValueFrom, map, Observable, of } from "rxjs"; -import { KeyDefinition, POLICIES_DISK, StateProvider } from "../../../platform/state"; +import { UserKeyDefinition, POLICIES_DISK, StateProvider } from "../../../platform/state"; import { PolicyId, UserId } from "../../../types/guid"; import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction"; import { InternalPolicyService as InternalPolicyServiceAbstraction } from "../../abstractions/policy/policy.service.abstraction"; @@ -14,8 +14,9 @@ import { ResetPasswordPolicyOptions } from "../../models/domain/reset-password-p const policyRecordToArray = (policiesMap: { [id: string]: PolicyData }) => Object.values(policiesMap || {}).map((f) => new Policy(f)); -export const POLICIES = KeyDefinition.record(POLICIES_DISK, "policies", { +export const POLICIES = UserKeyDefinition.record(POLICIES_DISK, "policies", { deserializer: (policyData) => policyData, + clearOn: ["logout"], }); export class PolicyService implements InternalPolicyServiceAbstraction { @@ -50,7 +51,7 @@ export class PolicyService implements InternalPolicyServiceAbstraction { map((policies) => policies.filter((p) => p.type === policyType)), ); - return combineLatest([filteredPolicies$, this.organizationService.organizations$]).pipe( + return combineLatest([filteredPolicies$, this.organizationService.getAll$(userId)]).pipe( map(([policies, organizations]) => this.enforcedPolicyFilter(policies, organizations)), ); } @@ -222,10 +223,6 @@ export class PolicyService implements InternalPolicyServiceAbstraction { await this.activeUserPolicyState.update(() => policies); } - async clear(userId?: UserId): Promise { - await this.stateProvider.setUserState(POLICIES, null, userId); - } - /** * Determines whether an orgUser is exempt from a specific policy because of their role * Generally orgUsers who can manage policies are exempt from them, but some policies are stricter @@ -235,6 +232,9 @@ export class PolicyService implements InternalPolicyServiceAbstraction { case PolicyType.MaximumVaultTimeout: // Max Vault Timeout applies to everyone except owners return organization.isOwner; + case PolicyType.PasswordGenerator: + // password generation policy applies to everyone + return false; default: return organization.canManagePolicies; } diff --git a/libs/common/src/admin-console/services/provider.service.ts b/libs/common/src/admin-console/services/provider.service.ts index 47291a5520..064e0c7175 100644 --- a/libs/common/src/admin-console/services/provider.service.ts +++ b/libs/common/src/admin-console/services/provider.service.ts @@ -1,13 +1,14 @@ import { Observable, map, firstValueFrom, of, switchMap, take } from "rxjs"; -import { KeyDefinition, PROVIDERS_DISK, StateProvider } from "../../platform/state"; +import { UserKeyDefinition, PROVIDERS_DISK, StateProvider } from "../../platform/state"; import { UserId } from "../../types/guid"; import { ProviderService as ProviderServiceAbstraction } from "../abstractions/provider.service"; import { ProviderData } from "../models/data/provider.data"; import { Provider } from "../models/domain/provider"; -export const PROVIDERS = KeyDefinition.record(PROVIDERS_DISK, "providers", { +export const PROVIDERS = UserKeyDefinition.record(PROVIDERS_DISK, "providers", { deserializer: (obj: ProviderData) => obj, + clearOn: ["logout"], }); function mapToSingleProvider(providerId: string) { diff --git a/libs/common/src/admin-console/services/provider/provider-api.service.ts b/libs/common/src/admin-console/services/provider/provider-api.service.ts new file mode 100644 index 0000000000..2ee921393f --- /dev/null +++ b/libs/common/src/admin-console/services/provider/provider-api.service.ts @@ -0,0 +1,47 @@ +import { ApiService } from "../../../abstractions/api.service"; +import { ProviderApiServiceAbstraction } from "../../abstractions/provider/provider-api.service.abstraction"; +import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request"; +import { ProviderUpdateRequest } from "../../models/request/provider/provider-update.request"; +import { ProviderVerifyRecoverDeleteRequest } from "../../models/request/provider/provider-verify-recover-delete.request"; +import { ProviderResponse } from "../../models/response/provider/provider.response"; + +export class ProviderApiService implements ProviderApiServiceAbstraction { + constructor(private apiService: ApiService) {} + async postProviderSetup(id: string, request: ProviderSetupRequest) { + const r = await this.apiService.send( + "POST", + "/providers/" + id + "/setup", + request, + true, + true, + ); + return new ProviderResponse(r); + } + + async getProvider(id: string) { + const r = await this.apiService.send("GET", "/providers/" + id, null, true, true); + return new ProviderResponse(r); + } + + async putProvider(id: string, request: ProviderUpdateRequest) { + const r = await this.apiService.send("PUT", "/providers/" + id, request, true, true); + return new ProviderResponse(r); + } + + providerRecoverDeleteToken( + providerId: string, + request: ProviderVerifyRecoverDeleteRequest, + ): Promise { + return this.apiService.send( + "POST", + "/providers/" + providerId + "/delete-recover-token", + request, + false, + false, + ); + } + + async deleteProvider(id: string): Promise { + await this.apiService.send("DELETE", "/providers/" + id, null, true, false); + } +} diff --git a/libs/common/src/auth/abstractions/account.service.ts b/libs/common/src/auth/abstractions/account.service.ts index 4e2a462755..fa9ad36378 100644 --- a/libs/common/src/auth/abstractions/account.service.ts +++ b/libs/common/src/auth/abstractions/account.service.ts @@ -1,27 +1,23 @@ import { Observable } from "rxjs"; import { UserId } from "../../types/guid"; -import { AuthenticationStatus } from "../enums/authentication-status"; /** * Holds information about an account for use in the AccountService * if more information is added, be sure to update the equality method. */ export type AccountInfo = { - status: AuthenticationStatus; email: string; name: string | undefined; }; export function accountInfoEqual(a: AccountInfo, b: AccountInfo) { - return a?.status === b?.status && a?.email === b?.email && a?.name === b?.name; + return a?.email === b?.email && a?.name === b?.name; } export abstract class AccountService { accounts$: Observable>; activeAccount$: Observable<{ id: UserId | undefined } & AccountInfo>; - accountLock$: Observable; - accountLogout$: Observable; /** * Updates the `accounts$` observable with the new account data. * @param userId @@ -40,24 +36,6 @@ export abstract class AccountService { * @param email */ abstract setAccountEmail(userId: UserId, email: string): Promise; - /** - * Updates the `accounts$` observable with the new account status. - * Also emits the `accountLock$` or `accountLogout$` observable if the status is `Locked` or `LoggedOut` respectively. - * @param userId - * @param status - */ - abstract setAccountStatus(userId: UserId, status: AuthenticationStatus): Promise; - /** - * Updates the `accounts$` observable with the new account status if the current status is higher than the `maxStatus`. - * - * This method only downgrades status to the maximum value sent in, it will not increase authentication status. - * - * @example An account is transitioning from unlocked to logged out. If callbacks that set the status to locked occur - * after it is updated to logged out, the account will be in the incorrect state. - * @param userId The user id of the account to be updated. - * @param maxStatus The new status of the account. - */ - abstract setMaxAccountStatus(userId: UserId, maxStatus: AuthenticationStatus): Promise; /** * Updates the `activeAccount$` observable with the new active account. * @param userId diff --git a/libs/common/src/auth/abstractions/auth.service.ts b/libs/common/src/auth/abstractions/auth.service.ts index de08dbd4e9..36d5d219b2 100644 --- a/libs/common/src/auth/abstractions/auth.service.ts +++ b/libs/common/src/auth/abstractions/auth.service.ts @@ -6,6 +6,8 @@ import { AuthenticationStatus } from "../enums/authentication-status"; export abstract class AuthService { /** Authentication status for the active user */ abstract activeAccountStatus$: Observable; + /** Authentication status for all known users */ + abstract authStatuses$: Observable>; /** * Returns an observable authentication status for the given user id. * @note userId is a required parameter, null values will always return `AuthenticationStatus.LoggedOut` diff --git a/libs/common/src/auth/abstractions/master-password.service.abstraction.ts b/libs/common/src/auth/abstractions/master-password.service.abstraction.ts new file mode 100644 index 0000000000..b36c8bfaae --- /dev/null +++ b/libs/common/src/auth/abstractions/master-password.service.abstraction.ts @@ -0,0 +1,82 @@ +import { Observable } from "rxjs"; + +import { EncString } from "../../platform/models/domain/enc-string"; +import { UserId } from "../../types/guid"; +import { MasterKey } from "../../types/key"; +import { ForceSetPasswordReason } from "../models/domain/force-set-password-reason"; + +export abstract class MasterPasswordServiceAbstraction { + /** + * An observable that emits if the user is being forced to set a password on login and why. + * @param userId The user ID. + * @throws If the user ID is missing. + */ + abstract forceSetPasswordReason$: (userId: UserId) => Observable; + /** + * An observable that emits the master key for the user. + * @param userId The user ID. + * @throws If the user ID is missing. + */ + abstract masterKey$: (userId: UserId) => Observable; + /** + * An observable that emits the master key hash for the user. + * @param userId The user ID. + * @throws If the user ID is missing. + */ + abstract masterKeyHash$: (userId: UserId) => Observable; + /** + * Returns the master key encrypted user key for the user. + * @param userId The user ID. + * @throws If the user ID is missing. + */ + abstract getMasterKeyEncryptedUserKey: (userId: UserId) => Promise; +} + +export abstract class InternalMasterPasswordServiceAbstraction extends MasterPasswordServiceAbstraction { + /** + * Set the master key for the user. + * Note: Use {@link clearMasterKey} to clear the master key. + * @param masterKey The master key. + * @param userId The user ID. + * @throws If the user ID or master key is missing. + */ + abstract setMasterKey: (masterKey: MasterKey, userId: UserId) => Promise; + /** + * Clear the master key for the user. + * @param userId The user ID. + * @throws If the user ID is missing. + */ + abstract clearMasterKey: (userId: UserId) => Promise; + /** + * Set the master key hash for the user. + * Note: Use {@link clearMasterKeyHash} to clear the master key hash. + * @param masterKeyHash The master key hash. + * @param userId The user ID. + * @throws If the user ID or master key hash is missing. + */ + abstract setMasterKeyHash: (masterKeyHash: string, userId: UserId) => Promise; + /** + * Clear the master key hash for the user. + * @param userId The user ID. + * @throws If the user ID is missing. + */ + abstract clearMasterKeyHash: (userId: UserId) => Promise; + + /** + * Set the master key encrypted user key for the user. + * @param encryptedKey The master key encrypted user key. + * @param userId The user ID. + * @throws If the user ID or encrypted key is missing. + */ + abstract setMasterKeyEncryptedUserKey: (encryptedKey: EncString, userId: UserId) => Promise; + /** + * Set the force set password reason for the user. + * @param reason The reason the user is being forced to set a password. + * @param userId The user ID. + * @throws If the user ID or reason is missing. + */ + abstract setForceSetPasswordReason: ( + reason: ForceSetPasswordReason, + userId: UserId, + ) => Promise; +} diff --git a/libs/common/src/auth/abstractions/token.service.ts b/libs/common/src/auth/abstractions/token.service.ts index 75bb383882..fc3bd317f4 100644 --- a/libs/common/src/auth/abstractions/token.service.ts +++ b/libs/common/src/auth/abstractions/token.service.ts @@ -213,4 +213,10 @@ export abstract class TokenService { * @returns A promise that resolves with a boolean representing the user's external authN status. */ getIsExternal: () => Promise; + + /** Gets the active or passed in user's security stamp */ + getSecurityStamp: (userId?: UserId) => Promise; + + /** Sets the security stamp for the active or passed in user */ + setSecurityStamp: (securityStamp: string, userId?: UserId) => Promise; } diff --git a/libs/common/src/auth/models/domain/admin-auth-req-storable.ts b/libs/common/src/auth/models/domain/admin-auth-req-storable.ts index 1eae7eeab1..df0341ac16 100644 --- a/libs/common/src/auth/models/domain/admin-auth-req-storable.ts +++ b/libs/common/src/auth/models/domain/admin-auth-req-storable.ts @@ -1,11 +1,7 @@ +import { Jsonify } from "type-fest"; + import { Utils } from "../../../platform/misc/utils"; -// TODO: Tech Debt: potentially create a type Storage shape vs using a class here in the future -// type StorageShape { -// id: string; -// privateKey: string; -// } -// so we can get rid of the any type passed into fromJSON and coming out of ToJSON export class AdminAuthRequestStorable { id: string; privateKey: Uint8Array; @@ -23,7 +19,7 @@ export class AdminAuthRequestStorable { }; } - static fromJSON(obj: any): AdminAuthRequestStorable { + static fromJSON(obj: Jsonify): AdminAuthRequestStorable { if (obj == null) { return null; } diff --git a/libs/common/src/auth/services/account.service.spec.ts b/libs/common/src/auth/services/account.service.spec.ts index e4195365f4..a9cec82c51 100644 --- a/libs/common/src/auth/services/account.service.spec.ts +++ b/libs/common/src/auth/services/account.service.spec.ts @@ -8,7 +8,6 @@ import { LogService } from "../../platform/abstractions/log.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; import { UserId } from "../../types/guid"; import { AccountInfo } from "../abstractions/account.service"; -import { AuthenticationStatus } from "../enums/authentication-status"; import { ACCOUNT_ACCOUNTS, @@ -24,9 +23,7 @@ describe("accountService", () => { let accountsState: FakeGlobalState>; let activeAccountIdState: FakeGlobalState; const userId = "userId" as UserId; - function userInfo(status: AuthenticationStatus): AccountInfo { - return { status, email: "email", name: "name" }; - } + const userInfo = { email: "email", name: "name" }; beforeEach(() => { messagingService = mock(); @@ -50,61 +47,49 @@ describe("accountService", () => { expect(emissions).toEqual([undefined]); }); - it("should emit the active account and status", async () => { + it("should emit the active account", async () => { const emissions = trackEmissions(sut.activeAccount$); - accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) }); + accountsState.stateSubject.next({ [userId]: userInfo }); activeAccountIdState.stateSubject.next(userId); expect(emissions).toEqual([ undefined, // initial value - { id: userId, ...userInfo(AuthenticationStatus.Unlocked) }, - ]); - }); - - it("should update the status if the account status changes", async () => { - accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) }); - activeAccountIdState.stateSubject.next(userId); - const emissions = trackEmissions(sut.activeAccount$); - accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Locked) }); - - expect(emissions).toEqual([ - { id: userId, ...userInfo(AuthenticationStatus.Unlocked) }, - { id: userId, ...userInfo(AuthenticationStatus.Locked) }, + { id: userId, ...userInfo }, ]); }); it("should remember the last emitted value", async () => { - accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) }); + accountsState.stateSubject.next({ [userId]: userInfo }); activeAccountIdState.stateSubject.next(userId); expect(await firstValueFrom(sut.activeAccount$)).toEqual({ id: userId, - ...userInfo(AuthenticationStatus.Unlocked), + ...userInfo, }); }); }); describe("accounts$", () => { it("should maintain an accounts cache", async () => { - accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) }); - accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Locked) }); + accountsState.stateSubject.next({ [userId]: userInfo }); + accountsState.stateSubject.next({ [userId]: userInfo }); expect(await firstValueFrom(sut.accounts$)).toEqual({ - [userId]: userInfo(AuthenticationStatus.Locked), + [userId]: userInfo, }); }); }); describe("addAccount", () => { it("should emit the new account", async () => { - await sut.addAccount(userId, userInfo(AuthenticationStatus.Unlocked)); + await sut.addAccount(userId, userInfo); const currentValue = await firstValueFrom(sut.accounts$); - expect(currentValue).toEqual({ [userId]: userInfo(AuthenticationStatus.Unlocked) }); + expect(currentValue).toEqual({ [userId]: userInfo }); }); }); describe("setAccountName", () => { - const initialState = { [userId]: userInfo(AuthenticationStatus.Unlocked) }; + const initialState = { [userId]: userInfo }; beforeEach(() => { accountsState.stateSubject.next(initialState); }); @@ -114,7 +99,7 @@ describe("accountService", () => { const currentState = await firstValueFrom(accountsState.state$); expect(currentState).toEqual({ - [userId]: { ...userInfo(AuthenticationStatus.Unlocked), name: "new name" }, + [userId]: { ...userInfo, name: "new name" }, }); }); @@ -127,7 +112,7 @@ describe("accountService", () => { }); describe("setAccountEmail", () => { - const initialState = { [userId]: userInfo(AuthenticationStatus.Unlocked) }; + const initialState = { [userId]: userInfo }; beforeEach(() => { accountsState.stateSubject.next(initialState); }); @@ -137,7 +122,7 @@ describe("accountService", () => { const currentState = await firstValueFrom(accountsState.state$); expect(currentState).toEqual({ - [userId]: { ...userInfo(AuthenticationStatus.Unlocked), email: "new email" }, + [userId]: { ...userInfo, email: "new email" }, }); }); @@ -149,49 +134,9 @@ describe("accountService", () => { }); }); - describe("setAccountStatus", () => { - const initialState = { [userId]: userInfo(AuthenticationStatus.Unlocked) }; - beforeEach(() => { - accountsState.stateSubject.next(initialState); - }); - - it("should update the account", async () => { - await sut.setAccountStatus(userId, AuthenticationStatus.Locked); - const currentState = await firstValueFrom(accountsState.state$); - - expect(currentState).toEqual({ - [userId]: { - ...userInfo(AuthenticationStatus.Unlocked), - status: AuthenticationStatus.Locked, - }, - }); - }); - - it("should not update if the status is the same", async () => { - await sut.setAccountStatus(userId, AuthenticationStatus.Unlocked); - const currentState = await firstValueFrom(accountsState.state$); - - expect(currentState).toEqual(initialState); - }); - - it("should emit logout if the status is logged out", async () => { - const emissions = trackEmissions(sut.accountLogout$); - await sut.setAccountStatus(userId, AuthenticationStatus.LoggedOut); - - expect(emissions).toEqual([userId]); - }); - - it("should emit lock if the status is locked", async () => { - const emissions = trackEmissions(sut.accountLock$); - await sut.setAccountStatus(userId, AuthenticationStatus.Locked); - - expect(emissions).toEqual([userId]); - }); - }); - describe("switchAccount", () => { beforeEach(() => { - accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) }); + accountsState.stateSubject.next({ [userId]: userInfo }); activeAccountIdState.stateSubject.next(userId); }); @@ -207,26 +152,4 @@ describe("accountService", () => { expect(sut.switchAccount("unknown" as UserId)).rejects.toThrowError("Account does not exist"); }); }); - - describe("setMaxAccountStatus", () => { - it("should update the account", async () => { - accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) }); - await sut.setMaxAccountStatus(userId, AuthenticationStatus.Locked); - const currentState = await firstValueFrom(accountsState.state$); - - expect(currentState).toEqual({ - [userId]: userInfo(AuthenticationStatus.Locked), - }); - }); - - it("should not update if the new max status is higher than the current", async () => { - accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.LoggedOut) }); - await sut.setMaxAccountStatus(userId, AuthenticationStatus.Locked); - const currentState = await firstValueFrom(accountsState.state$); - - expect(currentState).toEqual({ - [userId]: userInfo(AuthenticationStatus.LoggedOut), - }); - }); - }); }); diff --git a/libs/common/src/auth/services/account.service.ts b/libs/common/src/auth/services/account.service.ts index 8ef235d815..77d61fae91 100644 --- a/libs/common/src/auth/services/account.service.ts +++ b/libs/common/src/auth/services/account.service.ts @@ -14,7 +14,6 @@ import { KeyDefinition, } from "../../platform/state"; import { UserId } from "../../types/guid"; -import { AuthenticationStatus } from "../enums/authentication-status"; export const ACCOUNT_ACCOUNTS = KeyDefinition.record( ACCOUNT_MEMORY, @@ -36,8 +35,6 @@ export class AccountServiceImplementation implements InternalAccountService { accounts$; activeAccount$; - accountLock$ = this.lock.asObservable(); - accountLogout$ = this.logout.asObservable(); constructor( private messagingService: MessagingService, @@ -74,34 +71,6 @@ export class AccountServiceImplementation implements InternalAccountService { await this.setAccountInfo(userId, { email }); } - async setAccountStatus(userId: UserId, status: AuthenticationStatus): Promise { - await this.setAccountInfo(userId, { status }); - - if (status === AuthenticationStatus.LoggedOut) { - this.logout.next(userId); - } else if (status === AuthenticationStatus.Locked) { - this.lock.next(userId); - } - } - - async setMaxAccountStatus(userId: UserId, maxStatus: AuthenticationStatus): Promise { - await this.accountsState.update( - (accounts) => { - accounts[userId].status = maxStatus; - return accounts; - }, - { - shouldUpdate: (accounts) => { - if (accounts?.[userId] == null) { - throw new Error("Account does not exist"); - } - - return accounts[userId].status > maxStatus; - }, - }, - ); - } - async switchAccount(userId: UserId): Promise { await this.activeAccountIdState.update( (_, accounts) => { diff --git a/libs/common/src/auth/services/auth.service.spec.ts b/libs/common/src/auth/services/auth.service.spec.ts index 07e38def4b..3bdf85d3e1 100644 --- a/libs/common/src/auth/services/auth.service.spec.ts +++ b/libs/common/src/auth/services/auth.service.spec.ts @@ -122,6 +122,25 @@ describe("AuthService", () => { }); }); + describe("authStatuses$", () => { + it("requests auth status for all known users", async () => { + const userId2 = Utils.newGuid() as UserId; + + await accountService.addAccount(userId2, { email: "email2", name: "name2" }); + + const mockFn = jest.fn().mockReturnValue(of(AuthenticationStatus.Locked)); + sut.authStatusFor$ = mockFn; + + await expect(firstValueFrom(await sut.authStatuses$)).resolves.toEqual({ + [userId]: AuthenticationStatus.Locked, + [userId2]: AuthenticationStatus.Locked, + }); + expect(mockFn).toHaveBeenCalledTimes(2); + expect(mockFn).toHaveBeenCalledWith(userId); + expect(mockFn).toHaveBeenCalledWith(userId2); + }); + }); + describe("authStatusFor$", () => { beforeEach(() => { tokenService.hasAccessToken$.mockReturnValue(of(true)); diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index de5eb66c06..c9e711b4cc 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -12,7 +12,6 @@ import { ApiService } from "../../abstractions/api.service"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; import { StateService } from "../../platform/abstractions/state.service"; -import { KeySuffixOptions } from "../../platform/enums"; import { UserId } from "../../types/guid"; import { AccountService } from "../abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service"; @@ -21,6 +20,7 @@ import { AuthenticationStatus } from "../enums/authentication-status"; export class AuthService implements AuthServiceAbstraction { activeAccountStatus$: Observable; + authStatuses$: Observable>; constructor( protected accountService: AccountService, @@ -36,6 +36,26 @@ export class AuthService implements AuthServiceAbstraction { return this.authStatusFor$(userId); }), ); + + this.authStatuses$ = this.accountService.accounts$.pipe( + map((accounts) => Object.keys(accounts) as UserId[]), + switchMap((entries) => + combineLatest( + entries.map((userId) => + this.authStatusFor$(userId).pipe(map((status) => ({ userId, status }))), + ), + ), + ), + map((statuses) => { + return statuses.reduce( + (acc, { userId, status }) => { + acc[userId] = status; + return acc; + }, + {} as Record, + ); + }), + ); } authStatusFor$(userId: UserId): Observable { @@ -70,31 +90,11 @@ export class AuthService implements AuthServiceAbstraction { return AuthenticationStatus.LoggedOut; } - // If we don't have a user key in memory, we're locked - if (!(await this.cryptoService.hasUserKeyInMemory(userId))) { - // Check if the user has vault timeout set to never and verify that - // they've never unlocked their vault - const neverLock = - (await this.cryptoService.hasUserKeyStored(KeySuffixOptions.Auto, userId)) && - !(await this.stateService.getEverBeenUnlocked({ userId: userId })); + // Note: since we aggresively set the auto user key to memory if it exists on app init (see InitService) + // we only need to check if the user key is in memory. + const hasUserKey = await this.cryptoService.hasUserKeyInMemory(userId as UserId); - if (neverLock) { - // Attempt to get the key from storage and set it in memory - const userKey = await this.cryptoService.getUserKeyFromStorage( - KeySuffixOptions.Auto, - userId, - ); - await this.cryptoService.setUserKey(userKey, userId); - } - } - - // We do another check here in case setting the auto key failed - const hasKeyInMemory = await this.cryptoService.hasUserKeyInMemory(userId); - if (!hasKeyInMemory) { - return AuthenticationStatus.Locked; - } - - return AuthenticationStatus.Unlocked; + return hasUserKey ? AuthenticationStatus.Unlocked : AuthenticationStatus.Locked; } logOut(callback: () => void) { diff --git a/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts b/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts index e65c5cd499..6fb58eab28 100644 --- a/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts @@ -14,7 +14,7 @@ import { StorageLocation } from "../../platform/enums"; import { EncString } from "../../platform/models/domain/enc-string"; import { StorageOptions } from "../../platform/models/domain/storage-options"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; -import { DEVICE_TRUST_DISK_LOCAL, KeyDefinition, StateProvider } from "../../platform/state"; +import { DEVICE_TRUST_DISK_LOCAL, StateProvider, UserKeyDefinition } from "../../platform/state"; import { UserId } from "../../types/guid"; import { UserKey, DeviceKey } from "../../types/key"; import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction"; @@ -27,16 +27,18 @@ import { } from "../models/request/update-devices-trust.request"; /** Uses disk storage so that the device key can persist after log out and tab removal. */ -export const DEVICE_KEY = new KeyDefinition(DEVICE_TRUST_DISK_LOCAL, "deviceKey", { +export const DEVICE_KEY = new UserKeyDefinition(DEVICE_TRUST_DISK_LOCAL, "deviceKey", { deserializer: (deviceKey) => SymmetricCryptoKey.fromJSON(deviceKey) as DeviceKey, + clearOn: [], // Device key is needed to log back into device, so we can't clear it automatically during lock or logout }); /** Uses disk storage so that the shouldTrustDevice bool can persist across login. */ -export const SHOULD_TRUST_DEVICE = new KeyDefinition( +export const SHOULD_TRUST_DEVICE = new UserKeyDefinition( DEVICE_TRUST_DISK_LOCAL, "shouldTrustDevice", { deserializer: (shouldTrustDevice) => shouldTrustDevice, + clearOn: [], // Need to preserve the user setting, so we can't clear it automatically during lock or logout }, ); diff --git a/libs/common/src/auth/services/key-connector.service.spec.ts b/libs/common/src/auth/services/key-connector.service.spec.ts index 50fed856f9..e3e5fbdbe7 100644 --- a/libs/common/src/auth/services/key-connector.service.spec.ts +++ b/libs/common/src/auth/services/key-connector.service.spec.ts @@ -21,6 +21,7 @@ import { CONVERT_ACCOUNT_TO_KEY_CONNECTOR, KeyConnectorService, } from "./key-connector.service"; +import { FakeMasterPasswordService } from "./master-password/fake-master-password.service"; import { TokenService } from "./token.service"; describe("KeyConnectorService", () => { @@ -36,6 +37,7 @@ describe("KeyConnectorService", () => { let stateProvider: FakeStateProvider; let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; const mockUserId = Utils.newGuid() as UserId; const mockOrgId = Utils.newGuid() as OrganizationId; @@ -47,10 +49,13 @@ describe("KeyConnectorService", () => { beforeEach(() => { jest.clearAllMocks(); + masterPasswordService = new FakeMasterPasswordService(); accountService = mockAccountServiceWith(mockUserId); stateProvider = new FakeStateProvider(accountService); keyConnectorService = new KeyConnectorService( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -214,7 +219,10 @@ describe("KeyConnectorService", () => { // Assert expect(apiService.getMasterKeyFromKeyConnector).toHaveBeenCalledWith(url); - expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey); + expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith( + masterKey, + expect.any(String), + ); }); it("should handle errors thrown during the process", async () => { @@ -241,10 +249,10 @@ describe("KeyConnectorService", () => { // Arrange const organization = organizationData(true, true, "https://key-connector-url.com", 2, false); const masterKey = getMockMasterKey(); + masterPasswordService.masterKeySubject.next(masterKey); const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization); - jest.spyOn(cryptoService, "getMasterKey").mockResolvedValue(masterKey); jest.spyOn(apiService, "postUserKeyToKeyConnector").mockResolvedValue(); // Act @@ -252,7 +260,6 @@ describe("KeyConnectorService", () => { // Assert expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled(); - expect(cryptoService.getMasterKey).toHaveBeenCalled(); expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith( organization.keyConnectorUrl, keyConnectorRequest, @@ -268,8 +275,8 @@ describe("KeyConnectorService", () => { const error = new Error("Failed to post user key to key connector"); organizationService.getAll.mockResolvedValue([organization]); + masterPasswordService.masterKeySubject.next(masterKey); jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization); - jest.spyOn(cryptoService, "getMasterKey").mockResolvedValue(masterKey); jest.spyOn(apiService, "postUserKeyToKeyConnector").mockRejectedValue(error); jest.spyOn(logService, "error"); @@ -280,7 +287,6 @@ describe("KeyConnectorService", () => { // Assert expect(logService.error).toHaveBeenCalledWith(error); expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled(); - expect(cryptoService.getMasterKey).toHaveBeenCalled(); expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith( organization.keyConnectorUrl, keyConnectorRequest, diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index d1502ce06c..f8e523cce4 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -16,7 +16,9 @@ import { UserKeyDefinition, } from "../../platform/state"; import { MasterKey } from "../../types/key"; +import { AccountService } from "../abstractions/account.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service"; +import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction"; import { TokenService } from "../abstractions/token.service"; import { KdfConfig } from "../models/domain/kdf-config"; import { KeyConnectorUserKeyRequest } from "../models/request/key-connector-user-key.request"; @@ -45,6 +47,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { private usesKeyConnectorState: ActiveUserState; private convertAccountToKeyConnectorState: ActiveUserState; constructor( + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private apiService: ApiService, private tokenService: TokenService, @@ -78,7 +82,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { async migrateUser() { const organization = await this.getManagingOrganization(); - const masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); try { @@ -99,7 +104,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { const masterKeyResponse = await this.apiService.getMasterKeyFromKeyConnector(url); const keyArr = Utils.fromB64ToArray(masterKeyResponse.key); const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey; - await this.cryptoService.setMasterKey(masterKey); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setMasterKey(masterKey, userId); } catch (e) { this.handleKeyConnectorError(e); } @@ -136,7 +142,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { kdfConfig, ); const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); - await this.cryptoService.setMasterKey(masterKey); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setMasterKey(masterKey, userId); const userKey = await this.cryptoService.makeUserKey(masterKey); await this.cryptoService.setUserKey(userKey[0]); diff --git a/libs/common/src/auth/services/master-password/fake-master-password.service.ts b/libs/common/src/auth/services/master-password/fake-master-password.service.ts new file mode 100644 index 0000000000..dd034ec50b --- /dev/null +++ b/libs/common/src/auth/services/master-password/fake-master-password.service.ts @@ -0,0 +1,64 @@ +import { mock } from "jest-mock-extended"; +import { ReplaySubject, Observable } from "rxjs"; + +import { EncString } from "../../../platform/models/domain/enc-string"; +import { UserId } from "../../../types/guid"; +import { MasterKey } from "../../../types/key"; +import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction"; +import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason"; + +export class FakeMasterPasswordService implements InternalMasterPasswordServiceAbstraction { + mock = mock(); + + // eslint-disable-next-line rxjs/no-exposed-subjects -- test class + masterKeySubject = new ReplaySubject(1); + // eslint-disable-next-line rxjs/no-exposed-subjects -- test class + masterKeyHashSubject = new ReplaySubject(1); + // eslint-disable-next-line rxjs/no-exposed-subjects -- test class + forceSetPasswordReasonSubject = new ReplaySubject(1); + + constructor(initialMasterKey?: MasterKey, initialMasterKeyHash?: string) { + this.masterKeySubject.next(initialMasterKey); + this.masterKeyHashSubject.next(initialMasterKeyHash); + } + + masterKey$(userId: UserId): Observable { + return this.masterKeySubject.asObservable(); + } + + setMasterKey(masterKey: MasterKey, userId: UserId): Promise { + return this.mock.setMasterKey(masterKey, userId); + } + + clearMasterKey(userId: UserId): Promise { + return this.mock.clearMasterKey(userId); + } + + masterKeyHash$(userId: UserId): Observable { + return this.masterKeyHashSubject.asObservable(); + } + + getMasterKeyEncryptedUserKey(userId: UserId): Promise { + return this.mock.getMasterKeyEncryptedUserKey(userId); + } + + setMasterKeyEncryptedUserKey(encryptedKey: EncString, userId: UserId): Promise { + return this.mock.setMasterKeyEncryptedUserKey(encryptedKey, userId); + } + + setMasterKeyHash(masterKeyHash: string, userId: UserId): Promise { + return this.mock.setMasterKeyHash(masterKeyHash, userId); + } + + clearMasterKeyHash(userId: UserId): Promise { + return this.mock.clearMasterKeyHash(userId); + } + + forceSetPasswordReason$(userId: UserId): Observable { + return this.forceSetPasswordReasonSubject.asObservable(); + } + + setForceSetPasswordReason(reason: ForceSetPasswordReason, userId: UserId): Promise { + return this.mock.setForceSetPasswordReason(reason, userId); + } +} diff --git a/libs/common/src/auth/services/master-password/master-password.service.ts b/libs/common/src/auth/services/master-password/master-password.service.ts new file mode 100644 index 0000000000..fad48abc12 --- /dev/null +++ b/libs/common/src/auth/services/master-password/master-password.service.ts @@ -0,0 +1,140 @@ +import { firstValueFrom, map, Observable } from "rxjs"; + +import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { + MASTER_PASSWORD_DISK, + MASTER_PASSWORD_MEMORY, + StateProvider, + UserKeyDefinition, +} from "../../../platform/state"; +import { UserId } from "../../../types/guid"; +import { MasterKey } from "../../../types/key"; +import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction"; +import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason"; + +/** Memory since master key shouldn't be available on lock */ +const MASTER_KEY = new UserKeyDefinition(MASTER_PASSWORD_MEMORY, "masterKey", { + deserializer: (masterKey) => SymmetricCryptoKey.fromJSON(masterKey) as MasterKey, + clearOn: ["lock", "logout"], +}); + +/** Disk since master key hash is used for unlock */ +const MASTER_KEY_HASH = new UserKeyDefinition(MASTER_PASSWORD_DISK, "masterKeyHash", { + deserializer: (masterKeyHash) => masterKeyHash, + clearOn: ["logout"], +}); + +/** Disk to persist through lock */ +const MASTER_KEY_ENCRYPTED_USER_KEY = new UserKeyDefinition( + MASTER_PASSWORD_DISK, + "masterKeyEncryptedUserKey", + { + deserializer: (key) => key, + clearOn: ["logout"], + }, +); + +/** Disk to persist through lock and account switches */ +const FORCE_SET_PASSWORD_REASON = new UserKeyDefinition( + MASTER_PASSWORD_DISK, + "forceSetPasswordReason", + { + deserializer: (reason) => reason, + clearOn: ["logout"], + }, +); + +export class MasterPasswordService implements InternalMasterPasswordServiceAbstraction { + constructor(private stateProvider: StateProvider) {} + + masterKey$(userId: UserId): Observable { + if (userId == null) { + throw new Error("User ID is required."); + } + return this.stateProvider.getUser(userId, MASTER_KEY).state$; + } + + masterKeyHash$(userId: UserId): Observable { + if (userId == null) { + throw new Error("User ID is required."); + } + return this.stateProvider.getUser(userId, MASTER_KEY_HASH).state$; + } + + forceSetPasswordReason$(userId: UserId): Observable { + if (userId == null) { + throw new Error("User ID is required."); + } + return this.stateProvider + .getUser(userId, FORCE_SET_PASSWORD_REASON) + .state$.pipe(map((reason) => reason ?? ForceSetPasswordReason.None)); + } + + // TODO: Remove this method and decrypt directly in the service instead + async getMasterKeyEncryptedUserKey(userId: UserId): Promise { + if (userId == null) { + throw new Error("User ID is required."); + } + const key = await firstValueFrom( + this.stateProvider.getUser(userId, MASTER_KEY_ENCRYPTED_USER_KEY).state$, + ); + return EncString.fromJSON(key); + } + + async setMasterKey(masterKey: MasterKey, userId: UserId): Promise { + if (masterKey == null) { + throw new Error("Master key is required."); + } + if (userId == null) { + throw new Error("User ID is required."); + } + await this.stateProvider.getUser(userId, MASTER_KEY).update((_) => masterKey); + } + + async clearMasterKey(userId: UserId): Promise { + if (userId == null) { + throw new Error("User ID is required."); + } + await this.stateProvider.getUser(userId, MASTER_KEY).update((_) => null); + } + + async setMasterKeyHash(masterKeyHash: string, userId: UserId): Promise { + if (masterKeyHash == null) { + throw new Error("Master key hash is required."); + } + if (userId == null) { + throw new Error("User ID is required."); + } + await this.stateProvider.getUser(userId, MASTER_KEY_HASH).update((_) => masterKeyHash); + } + + async clearMasterKeyHash(userId: UserId): Promise { + if (userId == null) { + throw new Error("User ID is required."); + } + await this.stateProvider.getUser(userId, MASTER_KEY_HASH).update((_) => null); + } + + async setMasterKeyEncryptedUserKey(encryptedKey: EncString, userId: UserId): Promise { + if (encryptedKey == null) { + throw new Error("Encrypted Key is required."); + } + if (userId == null) { + throw new Error("User ID is required."); + } + await this.stateProvider + .getUser(userId, MASTER_KEY_ENCRYPTED_USER_KEY) + .update((_) => encryptedKey.toJSON() as EncryptedString); + } + + async setForceSetPasswordReason(reason: ForceSetPasswordReason, userId: UserId): Promise { + if (reason == null) { + throw new Error("Reason is required."); + } + if (userId == null) { + throw new Error("User ID is required."); + } + await this.stateProvider.getUser(userId, FORCE_SET_PASSWORD_REASON).update((_) => reason); + } +} diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts index 408ed33c97..fc5060af5f 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts @@ -8,7 +8,6 @@ import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models import { CryptoService } from "../../platform/abstractions/crypto.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { AccountInfo, AccountService } from "../abstractions/account.service"; -import { AuthenticationStatus } from "../enums/authentication-status"; import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation"; @@ -91,7 +90,6 @@ describe("PasswordResetEnrollmentServiceImplementation", () => { const user1AccountInfo: AccountInfo = { name: "Test User 1", email: "test1@email.com", - status: AuthenticationStatus.Unlocked, }; activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId })); diff --git a/libs/common/src/auth/services/sso-login.service.ts b/libs/common/src/auth/services/sso-login.service.ts index 99640e1c6c..3df6ef3540 100644 --- a/libs/common/src/auth/services/sso-login.service.ts +++ b/libs/common/src/auth/services/sso-login.service.ts @@ -6,6 +6,7 @@ import { KeyDefinition, SSO_DISK, StateProvider, + UserKeyDefinition, } from "../../platform/state"; import { SsoLoginServiceAbstraction } from "../abstractions/sso-login.service.abstraction"; @@ -26,7 +27,19 @@ const SSO_STATE = new KeyDefinition(SSO_DISK, "ssoState", { /** * Uses disk storage so that the organization sso identifier can be persisted across sso redirects. */ -const ORGANIZATION_SSO_IDENTIFIER = new KeyDefinition( +const USER_ORGANIZATION_SSO_IDENTIFIER = new UserKeyDefinition( + SSO_DISK, + "organizationSsoIdentifier", + { + deserializer: (organizationIdentifier) => organizationIdentifier, + clearOn: ["logout"], // Used for login, so not needed past logout + }, +); + +/** + * Uses disk storage so that the organization sso identifier can be persisted across sso redirects. + */ +const GLOBAL_ORGANIZATION_SSO_IDENTIFIER = new KeyDefinition( SSO_DISK, "organizationSsoIdentifier", { @@ -51,10 +64,10 @@ export class SsoLoginService implements SsoLoginServiceAbstraction { constructor(private stateProvider: StateProvider) { this.codeVerifierState = this.stateProvider.getGlobal(CODE_VERIFIER); this.ssoState = this.stateProvider.getGlobal(SSO_STATE); - this.orgSsoIdentifierState = this.stateProvider.getGlobal(ORGANIZATION_SSO_IDENTIFIER); + this.orgSsoIdentifierState = this.stateProvider.getGlobal(GLOBAL_ORGANIZATION_SSO_IDENTIFIER); this.ssoEmailState = this.stateProvider.getGlobal(SSO_EMAIL); this.activeUserOrgSsoIdentifierState = this.stateProvider.getActive( - ORGANIZATION_SSO_IDENTIFIER, + USER_ORGANIZATION_SSO_IDENTIFIER, ); } diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index c409263209..3e92053d2f 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -23,7 +23,7 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, - REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE, + SECURITY_STAMP_MEMORY, } from "./token.state"; describe("TokenService", () => { @@ -1120,20 +1120,13 @@ describe("TokenService", () => { secureStorageOptions, ); - // assert data was migrated out of disk and memory + flag was set + // assert data was migrated out of disk and memory expect( singleUserStateProvider.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK).nextMock, ).toHaveBeenCalledWith(null); expect( singleUserStateProvider.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY).nextMock, ).toHaveBeenCalledWith(null); - - expect( - singleUserStateProvider.getFake( - userIdFromAccessToken, - REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE, - ).nextMock, - ).toHaveBeenCalledWith(true); }); }); }); @@ -1260,11 +1253,6 @@ describe("TokenService", () => { .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) .stateSubject.next(userIdFromAccessToken); - // set access token migration flag to true - singleUserStateProvider - .getFake(userIdFromAccessToken, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE) - .stateSubject.next([userIdFromAccessToken, true]); - // Act const result = await tokenService.getRefreshToken(); // Assert @@ -1284,11 +1272,6 @@ describe("TokenService", () => { secureStorageService.get.mockResolvedValue(refreshToken); - // set access token migration flag to true - singleUserStateProvider - .getFake(userIdFromAccessToken, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE) - .stateSubject.next([userIdFromAccessToken, true]); - // Act const result = await tokenService.getRefreshToken(userIdFromAccessToken); // Assert @@ -1305,11 +1288,6 @@ describe("TokenService", () => { .getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK) .stateSubject.next([userIdFromAccessToken, refreshToken]); - // set refresh token migration flag to false - singleUserStateProvider - .getFake(userIdFromAccessToken, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE) - .stateSubject.next([userIdFromAccessToken, false]); - // Act const result = await tokenService.getRefreshToken(userIdFromAccessToken); @@ -1335,11 +1313,6 @@ describe("TokenService", () => { .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) .stateSubject.next(userIdFromAccessToken); - // set access token migration flag to false - singleUserStateProvider - .getFake(userIdFromAccessToken, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE) - .stateSubject.next([userIdFromAccessToken, false]); - // Act const result = await tokenService.getRefreshToken(); @@ -2219,6 +2192,84 @@ describe("TokenService", () => { }); }); + describe("Security Stamp methods", () => { + const mockSecurityStamp = "securityStamp"; + + describe("setSecurityStamp", () => { + it("should throw an error if no user id is provided and there is no active user in global state", async () => { + // Act + // note: don't await here because we want to test the error + const result = tokenService.setSecurityStamp(mockSecurityStamp); + // Assert + await expect(result).rejects.toThrow("User id not found. Cannot set security stamp."); + }); + + it("should set the security stamp in memory when there is an active user in global state", async () => { + // Arrange + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + + // Act + await tokenService.setSecurityStamp(mockSecurityStamp); + + // Assert + expect( + singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock, + ).toHaveBeenCalledWith(mockSecurityStamp); + }); + + it("should set the security stamp in memory for the specified user id", async () => { + // Act + await tokenService.setSecurityStamp(mockSecurityStamp, userIdFromAccessToken); + + // Assert + expect( + singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock, + ).toHaveBeenCalledWith(mockSecurityStamp); + }); + }); + + describe("getSecurityStamp", () => { + it("should throw an error if no user id is provided and there is no active user in global state", async () => { + // Act + // note: don't await here because we want to test the error + const result = tokenService.getSecurityStamp(); + // Assert + await expect(result).rejects.toThrow("User id not found. Cannot get security stamp."); + }); + + it("should return the security stamp from memory with no user id specified (uses global active user)", async () => { + // Arrange + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + + singleUserStateProvider + .getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY) + .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]); + + // Act + const result = await tokenService.getSecurityStamp(); + + // Assert + expect(result).toEqual(mockSecurityStamp); + }); + + it("should return the security stamp from memory for the specified user id", async () => { + // Arrange + singleUserStateProvider + .getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY) + .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]); + + // Act + const result = await tokenService.getSecurityStamp(userIdFromAccessToken); + // Assert + expect(result).toEqual(mockSecurityStamp); + }); + }); + }); + // Helpers function createTokenService(supportsSecureStorage: boolean) { return new TokenService( diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index fb13c21870..40036a8453 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -15,8 +15,8 @@ import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypt import { GlobalState, GlobalStateProvider, - KeyDefinition, SingleUserStateProvider, + UserKeyDefinition, } from "../../platform/state"; import { UserId } from "../../types/guid"; import { TokenService as TokenServiceAbstraction } from "../abstractions/token.service"; @@ -32,7 +32,7 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, - REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE, + SECURITY_STAMP_MEMORY, } from "./token.state"; export enum TokenStorageLocation { @@ -441,9 +441,6 @@ export class TokenService implements TokenServiceAbstraction { await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_DISK).update((_) => null); await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_MEMORY).update((_) => null); - // Set flag to indicate that the refresh token has been migrated to secure storage (don't remove this) - await this.setRefreshTokenMigratedToSecureStorage(userId); - return; case TokenStorageLocation.Disk: @@ -467,12 +464,6 @@ export class TokenService implements TokenServiceAbstraction { return undefined; } - const refreshTokenMigratedToSecureStorage = - await this.getRefreshTokenMigratedToSecureStorage(userId); - if (this.platformSupportsSecureStorage && refreshTokenMigratedToSecureStorage) { - return await this.getStringFromSecureStorage(userId, this.refreshTokenSecureStorageKey); - } - // pre-secure storage migration: // Always read memory first b/c faster const refreshTokenMemory = await this.getStateValueByUserIdAndKeyDef( @@ -484,13 +475,24 @@ export class TokenService implements TokenServiceAbstraction { return refreshTokenMemory; } - // if memory is null, read from disk + // if memory is null, read from disk and then secure storage const refreshTokenDisk = await this.getStateValueByUserIdAndKeyDef(userId, REFRESH_TOKEN_DISK); if (refreshTokenDisk != null) { return refreshTokenDisk; } + if (this.platformSupportsSecureStorage) { + const refreshTokenSecureStorage = await this.getStringFromSecureStorage( + userId, + this.refreshTokenSecureStorageKey, + ); + + if (refreshTokenSecureStorage != null) { + return refreshTokenSecureStorage; + } + } + return null; } @@ -516,18 +518,6 @@ export class TokenService implements TokenServiceAbstraction { await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_DISK).update((_) => null); } - private async getRefreshTokenMigratedToSecureStorage(userId: UserId): Promise { - return await firstValueFrom( - this.singleUserStateProvider.get(userId, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE).state$, - ); - } - - private async setRefreshTokenMigratedToSecureStorage(userId: UserId): Promise { - await this.singleUserStateProvider - .get(userId, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE) - .update((_) => true); - } - async setClientId( clientId: string, vaultTimeoutAction: VaultTimeoutAction, @@ -861,9 +851,33 @@ export class TokenService implements TokenServiceAbstraction { return Array.isArray(decoded.amr) && decoded.amr.includes("external"); } + async getSecurityStamp(userId?: UserId): Promise { + userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); + + if (!userId) { + throw new Error("User id not found. Cannot get security stamp."); + } + + const securityStamp = await this.getStateValueByUserIdAndKeyDef(userId, SECURITY_STAMP_MEMORY); + + return securityStamp; + } + + async setSecurityStamp(securityStamp: string, userId?: UserId): Promise { + userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); + + if (!userId) { + throw new Error("User id not found. Cannot set security stamp."); + } + + await this.singleUserStateProvider + .get(userId, SECURITY_STAMP_MEMORY) + .update((_) => securityStamp); + } + private async getStateValueByUserIdAndKeyDef( userId: UserId, - storageLocation: KeyDefinition, + storageLocation: UserKeyDefinition, ): Promise { // read from single user state provider return await firstValueFrom(this.singleUserStateProvider.get(userId, storageLocation).state$); diff --git a/libs/common/src/auth/services/token.state.spec.ts b/libs/common/src/auth/services/token.state.spec.ts index 24eddc73f5..bb82410fac 100644 --- a/libs/common/src/auth/services/token.state.spec.ts +++ b/libs/common/src/auth/services/token.state.spec.ts @@ -1,4 +1,4 @@ -import { KeyDefinition } from "../../platform/state"; +import { KeyDefinition, UserKeyDefinition } from "../../platform/state"; import { ACCESS_TOKEN_DISK, @@ -10,7 +10,7 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, - REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE, + SECURITY_STAMP_MEMORY, } from "./token.state"; describe.each([ @@ -18,18 +18,18 @@ describe.each([ [ACCESS_TOKEN_MEMORY, "accessTokenMemory"], [REFRESH_TOKEN_DISK, "refreshTokenDisk"], [REFRESH_TOKEN_MEMORY, "refreshTokenMemory"], - [REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE, true], [EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, { user: "token" }], [API_KEY_CLIENT_ID_DISK, "apiKeyClientIdDisk"], [API_KEY_CLIENT_ID_MEMORY, "apiKeyClientIdMemory"], [API_KEY_CLIENT_SECRET_DISK, "apiKeyClientSecretDisk"], [API_KEY_CLIENT_SECRET_MEMORY, "apiKeyClientSecretMemory"], + [SECURITY_STAMP_MEMORY, "securityStamp"], ])( "deserializes state key definitions", ( keyDefinition: - | KeyDefinition - | KeyDefinition + | UserKeyDefinition + | UserKeyDefinition | KeyDefinition>, state: string | boolean | Record, ) => { @@ -50,7 +50,10 @@ describe.each([ return typeof value === "object" && value !== null && !Array.isArray(value); } - function testDeserialization(keyDefinition: KeyDefinition, state: T) { + function testDeserialization( + keyDefinition: KeyDefinition | UserKeyDefinition, + state: T, + ) { const deserialized = keyDefinition.deserializer(JSON.parse(JSON.stringify(state))); expect(deserialized).toEqual(state); } diff --git a/libs/common/src/auth/services/token.state.ts b/libs/common/src/auth/services/token.state.ts index 368f3c4ca2..57d85f2a55 100644 --- a/libs/common/src/auth/services/token.state.ts +++ b/libs/common/src/auth/services/token.state.ts @@ -1,33 +1,35 @@ -import { KeyDefinition, TOKEN_DISK, TOKEN_DISK_LOCAL, TOKEN_MEMORY } from "../../platform/state"; +import { + KeyDefinition, + TOKEN_DISK, + TOKEN_DISK_LOCAL, + TOKEN_MEMORY, + UserKeyDefinition, +} from "../../platform/state"; // Note: all tokens / API key information must be cleared on logout. // because we are using secure storage, we must manually call to clean up our tokens. // See stateService.deAuthenticateAccount for where we call clearTokens(...) -export const ACCESS_TOKEN_DISK = new KeyDefinition(TOKEN_DISK, "accessToken", { +export const ACCESS_TOKEN_DISK = new UserKeyDefinition(TOKEN_DISK, "accessToken", { deserializer: (accessToken) => accessToken, + clearOn: [], // Manually handled }); -export const ACCESS_TOKEN_MEMORY = new KeyDefinition(TOKEN_MEMORY, "accessToken", { +export const ACCESS_TOKEN_MEMORY = new UserKeyDefinition(TOKEN_MEMORY, "accessToken", { deserializer: (accessToken) => accessToken, + clearOn: [], // Manually handled }); -export const REFRESH_TOKEN_DISK = new KeyDefinition(TOKEN_DISK, "refreshToken", { +export const REFRESH_TOKEN_DISK = new UserKeyDefinition(TOKEN_DISK, "refreshToken", { deserializer: (refreshToken) => refreshToken, + clearOn: [], // Manually handled }); -export const REFRESH_TOKEN_MEMORY = new KeyDefinition(TOKEN_MEMORY, "refreshToken", { +export const REFRESH_TOKEN_MEMORY = new UserKeyDefinition(TOKEN_MEMORY, "refreshToken", { deserializer: (refreshToken) => refreshToken, + clearOn: [], // Manually handled }); -export const REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE = new KeyDefinition( - TOKEN_DISK, - "refreshTokenMigratedToSecureStorage", - { - deserializer: (refreshTokenMigratedToSecureStorage) => refreshTokenMigratedToSecureStorage, - }, -); - export const EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL = KeyDefinition.record( TOKEN_DISK_LOCAL, "emailTwoFactorTokenRecord", @@ -36,26 +38,39 @@ export const EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL = KeyDefinition.record(TOKEN_DISK, "apiKeyClientId", { +export const API_KEY_CLIENT_ID_DISK = new UserKeyDefinition(TOKEN_DISK, "apiKeyClientId", { deserializer: (apiKeyClientId) => apiKeyClientId, + clearOn: [], // Manually handled }); -export const API_KEY_CLIENT_ID_MEMORY = new KeyDefinition(TOKEN_MEMORY, "apiKeyClientId", { - deserializer: (apiKeyClientId) => apiKeyClientId, -}); +export const API_KEY_CLIENT_ID_MEMORY = new UserKeyDefinition( + TOKEN_MEMORY, + "apiKeyClientId", + { + deserializer: (apiKeyClientId) => apiKeyClientId, + clearOn: [], // Manually handled + }, +); -export const API_KEY_CLIENT_SECRET_DISK = new KeyDefinition( +export const API_KEY_CLIENT_SECRET_DISK = new UserKeyDefinition( TOKEN_DISK, "apiKeyClientSecret", { deserializer: (apiKeyClientSecret) => apiKeyClientSecret, + clearOn: [], // Manually handled }, ); -export const API_KEY_CLIENT_SECRET_MEMORY = new KeyDefinition( +export const API_KEY_CLIENT_SECRET_MEMORY = new UserKeyDefinition( TOKEN_MEMORY, "apiKeyClientSecret", { deserializer: (apiKeyClientSecret) => apiKeyClientSecret, + clearOn: [], // Manually handled }, ); + +export const SECURITY_STAMP_MEMORY = new UserKeyDefinition(TOKEN_MEMORY, "securityStamp", { + deserializer: (securityStamp) => securityStamp, + clearOn: ["logout"], +}); diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index 0b4cd96099..5a443b784d 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.ts @@ -10,7 +10,10 @@ import { LogService } from "../../../platform/abstractions/log.service"; import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; import { StateService } from "../../../platform/abstractions/state.service"; import { KeySuffixOptions } from "../../../platform/enums/key-suffix-options.enum"; +import { UserId } from "../../../types/guid"; import { UserKey } from "../../../types/key"; +import { AccountService } from "../../abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction"; import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction"; import { UserVerificationService as UserVerificationServiceAbstraction } from "../../abstractions/user-verification/user-verification.service.abstraction"; import { VerificationType } from "../../enums/verification-type"; @@ -35,6 +38,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti constructor( private stateService: StateService, private cryptoService: CryptoService, + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private i18nService: I18nService, private userVerificationApiService: UserVerificationApiServiceAbstraction, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, @@ -107,7 +112,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti if (verification.type === VerificationType.OTP) { request.otp = verification.secret; } else { - let masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (!masterKey && !alreadyHashed) { masterKey = await this.cryptoService.makeMasterKey( verification.secret, @@ -164,7 +170,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti private async verifyUserByMasterPassword( verification: MasterPasswordVerification, ): Promise { - let masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (!masterKey) { masterKey = await this.cryptoService.makeMasterKey( verification.secret, @@ -181,7 +188,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti throw new Error(this.i18nService.t("invalidMasterPassword")); } // TODO: we should re-evaluate later on if user verification should have the side effect of modifying state. Probably not. - await this.cryptoService.setMasterKey(masterKey); + await this.masterPasswordService.setMasterKey(masterKey, userId); return true; } @@ -230,9 +237,10 @@ export class UserVerificationService implements UserVerificationServiceAbstracti } async hasMasterPasswordAndMasterKeyHash(userId?: string): Promise { + userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id; return ( (await this.hasMasterPassword(userId)) && - (await this.cryptoService.getMasterKeyHash()) != null + (await firstValueFrom(this.masterPasswordService.masterKeyHash$(userId as UserId))) != null ); } diff --git a/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts b/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts index 1311976c4b..15f0d4b551 100644 --- a/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts @@ -1,6 +1,10 @@ import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response"; -import { ProviderSubscriptionUpdateRequest } from "../models/request/provider-subscription-update.request"; +import { OrganizationSubscriptionResponse } from "../../billing/models/response/organization-subscription.response"; +import { PlanResponse } from "../../billing/models/response/plan.response"; +import { ListResponse } from "../../models/response/list.response"; +import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; +import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request"; import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; export abstract class BillingApiServiceAbstraction { @@ -8,12 +12,21 @@ export abstract class BillingApiServiceAbstraction { organizationId: string, request: SubscriptionCancellationRequest, ) => Promise; + cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise; + createClientOrganization: ( + providerId: string, + request: CreateClientOrganizationRequest, + ) => Promise; getBillingStatus: (id: string) => Promise; - getProviderClientSubscriptions: (providerId: string) => Promise; - putProviderClientSubscriptions: ( + getOrganizationSubscription: ( + organizationId: string, + ) => Promise; + getPlans: () => Promise>; + getProviderSubscription: (providerId: string) => Promise; + updateClientOrganization: ( providerId: string, organizationId: string, - request: ProviderSubscriptionUpdateRequest, + request: UpdateClientOrganizationRequest, ) => Promise; } diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index d19724b600..0917025eec 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -41,6 +41,8 @@ export type SubscriptionInformation = { }; export abstract class OrganizationBillingServiceAbstraction { + isOnSecretsManagerStandalone: (organizationId: string) => Promise; + purchaseSubscription: (subscription: SubscriptionInformation) => Promise; startFree: (subscription: SubscriptionInformation) => Promise; diff --git a/libs/common/src/billing/enums/plan-type.enum.ts b/libs/common/src/billing/enums/plan-type.enum.ts index 38febc50e4..c897770345 100644 --- a/libs/common/src/billing/enums/plan-type.enum.ts +++ b/libs/common/src/billing/enums/plan-type.enum.ts @@ -11,9 +11,14 @@ export enum PlanType { TeamsAnnually2020 = 9, EnterpriseMonthly2020 = 10, EnterpriseAnnually2020 = 11, - TeamsMonthly = 12, - TeamsAnnually = 13, - EnterpriseMonthly = 14, - EnterpriseAnnually = 15, - TeamsStarter = 16, + TeamsMonthly2023 = 12, + TeamsAnnually2023 = 13, + EnterpriseMonthly2023 = 14, + EnterpriseAnnually2023 = 15, + TeamsStarter2023 = 16, + TeamsMonthly = 17, + TeamsAnnually = 18, + EnterpriseMonthly = 19, + EnterpriseAnnually = 20, + TeamsStarter = 21, } diff --git a/libs/common/src/billing/models/request/create-client-organization.request.ts b/libs/common/src/billing/models/request/create-client-organization.request.ts new file mode 100644 index 0000000000..2eac23531a --- /dev/null +++ b/libs/common/src/billing/models/request/create-client-organization.request.ts @@ -0,0 +1,12 @@ +import { OrganizationKeysRequest } from "../../../admin-console/models/request/organization-keys.request"; +import { PlanType } from "../../../billing/enums"; + +export class CreateClientOrganizationRequest { + name: string; + ownerEmail: string; + planType: PlanType; + seats: number; + key: string; + keyPair: OrganizationKeysRequest; + collectionName: string; +} diff --git a/libs/common/src/billing/models/request/organization-tax-info-update.request.ts b/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts similarity index 66% rename from libs/common/src/billing/models/request/organization-tax-info-update.request.ts rename to libs/common/src/billing/models/request/expanded-tax-info-update.request.ts index 0f8ec92160..6589b9c1df 100644 --- a/libs/common/src/billing/models/request/organization-tax-info-update.request.ts +++ b/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts @@ -1,6 +1,6 @@ import { TaxInfoUpdateRequest } from "./tax-info-update.request"; -export class OrganizationTaxInfoUpdateRequest extends TaxInfoUpdateRequest { +export class ExpandedTaxInfoUpdateRequest extends TaxInfoUpdateRequest { taxId: string; line1: string; line2: string; diff --git a/libs/common/src/billing/models/request/payment.request.ts b/libs/common/src/billing/models/request/payment.request.ts index d54ca91f62..e73a10bcea 100644 --- a/libs/common/src/billing/models/request/payment.request.ts +++ b/libs/common/src/billing/models/request/payment.request.ts @@ -1,8 +1,8 @@ import { PaymentMethodType } from "../../enums"; -import { OrganizationTaxInfoUpdateRequest } from "./organization-tax-info-update.request"; +import { ExpandedTaxInfoUpdateRequest } from "./expanded-tax-info-update.request"; -export class PaymentRequest extends OrganizationTaxInfoUpdateRequest { +export class PaymentRequest extends ExpandedTaxInfoUpdateRequest { paymentMethodType: PaymentMethodType; paymentToken: string; } diff --git a/libs/common/src/billing/models/request/provider-subscription-update.request.ts b/libs/common/src/billing/models/request/provider-subscription-update.request.ts deleted file mode 100644 index f2bf4c7e97..0000000000 --- a/libs/common/src/billing/models/request/provider-subscription-update.request.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class ProviderSubscriptionUpdateRequest { - assignedSeats: number; -} diff --git a/libs/common/src/billing/models/request/update-client-organization.request.ts b/libs/common/src/billing/models/request/update-client-organization.request.ts new file mode 100644 index 0000000000..16dbe1e17d --- /dev/null +++ b/libs/common/src/billing/models/request/update-client-organization.request.ts @@ -0,0 +1,3 @@ +export class UpdateClientOrganizationRequest { + assignedSeats: number; +} diff --git a/libs/common/src/billing/services/billing-api.service.ts b/libs/common/src/billing/services/billing-api.service.ts index 48866ab90d..1c119b971d 100644 --- a/libs/common/src/billing/services/billing-api.service.ts +++ b/libs/common/src/billing/services/billing-api.service.ts @@ -2,7 +2,11 @@ import { ApiService } from "../../abstractions/api.service"; import { BillingApiServiceAbstraction } from "../../billing/abstractions/billilng-api.service.abstraction"; import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response"; -import { ProviderSubscriptionUpdateRequest } from "../models/request/provider-subscription-update.request"; +import { OrganizationSubscriptionResponse } from "../../billing/models/response/organization-subscription.response"; +import { PlanResponse } from "../../billing/models/response/plan.response"; +import { ListResponse } from "../../models/response/list.response"; +import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; +import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request"; import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; export class BillingApiService implements BillingApiServiceAbstraction { @@ -25,6 +29,19 @@ export class BillingApiService implements BillingApiServiceAbstraction { return this.apiService.send("POST", "/accounts/cancel", request, true, false); } + createClientOrganization( + providerId: string, + request: CreateClientOrganizationRequest, + ): Promise { + return this.apiService.send( + "POST", + "/providers/" + providerId + "/clients", + request, + true, + false, + ); + } + async getBillingStatus(id: string): Promise { const r = await this.apiService.send( "GET", @@ -33,11 +50,28 @@ export class BillingApiService implements BillingApiServiceAbstraction { true, true, ); - return new OrganizationBillingStatusResponse(r); } - async getProviderClientSubscriptions(providerId: string): Promise { + async getOrganizationSubscription( + organizationId: string, + ): Promise { + const r = await this.apiService.send( + "GET", + "/organizations/" + organizationId + "/subscription", + null, + true, + true, + ); + return new OrganizationSubscriptionResponse(r); + } + + async getPlans(): Promise> { + const r = await this.apiService.send("GET", "/plans", null, false, true); + return new ListResponse(r, PlanResponse); + } + + async getProviderSubscription(providerId: string): Promise { const r = await this.apiService.send( "GET", "/providers/" + providerId + "/billing/subscription", @@ -48,14 +82,14 @@ export class BillingApiService implements BillingApiServiceAbstraction { return new ProviderSubscriptionResponse(r); } - async putProviderClientSubscriptions( + async updateClientOrganization( providerId: string, organizationId: string, - request: ProviderSubscriptionUpdateRequest, + request: UpdateClientOrganizationRequest, ): Promise { return await this.apiService.send( "PUT", - "/providers/" + providerId + "/organizations/" + organizationId, + "/providers/" + providerId + "/clients/" + organizationId, request, true, false, diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index a9437e288c..fb2084bb6a 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -1,3 +1,4 @@ +import { ApiService } from "../../abstractions/api.service"; import { OrganizationApiServiceAbstraction as OrganizationApiService } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; import { OrganizationKeysRequest } from "../../admin-console/models/request/organization-keys.request"; @@ -7,6 +8,8 @@ import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { EncString } from "../../platform/models/domain/enc-string"; import { OrgKey } from "../../types/key"; +import { SyncService } from "../../vault/abstractions/sync/sync.service.abstraction"; +import { BillingApiServiceAbstraction as BillingApiService } from "../abstractions/billilng-api.service.abstraction"; import { OrganizationBillingServiceAbstraction, OrganizationInformation, @@ -25,12 +28,28 @@ interface OrganizationKeys { export class OrganizationBillingService implements OrganizationBillingServiceAbstraction { constructor( + private apiService: ApiService, + private billingApiService: BillingApiService, private cryptoService: CryptoService, private encryptService: EncryptService, private i18nService: I18nService, private organizationApiService: OrganizationApiService, + private syncService: SyncService, ) {} + async isOnSecretsManagerStandalone(organizationId: string): Promise { + const response = await this.billingApiService.getOrganizationSubscription(organizationId); + if (response.customerDiscount?.id === "sm-standalone") { + const productIds = response.subscription.items.map((item) => item.productId); + return ( + response.customerDiscount?.appliesTo.filter((appliesToProductId) => + productIds.includes(appliesToProductId), + ).length > 0 + ); + } + return false; + } + async purchaseSubscription(subscription: SubscriptionInformation): Promise { const request = new OrganizationCreateRequest(); @@ -44,7 +63,13 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs this.setPaymentInformation(request, subscription.payment); - return await this.organizationApiService.create(request); + const response = await this.organizationApiService.create(request); + + await this.apiService.refreshIdentityToken(); + + await this.syncService.fullSync(true); + + return response; } async startFree(subscription: SubscriptionInformation): Promise { @@ -58,7 +83,13 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs this.setPlanInformation(request, subscription.plan); - return await this.organizationApiService.create(request); + const response = await this.organizationApiService.create(request); + + await this.apiService.refreshIdentityToken(); + + await this.syncService.fullSync(true); + + return response; } private async makeOrganizationKeys(): Promise { @@ -81,6 +112,7 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs case PlanType.Free: case PlanType.FamiliesAnnually: case PlanType.FamiliesAnnually2019: + case PlanType.TeamsStarter2023: case PlanType.TeamsStarter: return true; default: diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 9d427034bd..636e9bc4ce 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -9,6 +9,8 @@ export enum FeatureFlag { ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners", EnableConsolidatedBilling = "enable-consolidated-billing", AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section", + UnassignedItemsBanner = "unassigned-items-banner", + EnableDeleteProvider = "AC-1218-delete-provider", } // Replace this with a type safe lookup of the feature flag values in PM-2282 diff --git a/libs/common/src/enums/index.ts b/libs/common/src/enums/index.ts index 378af213e6..9ca806899a 100644 --- a/libs/common/src/enums/index.ts +++ b/libs/common/src/enums/index.ts @@ -3,6 +3,7 @@ export * from "./device-type.enum"; export * from "./event-system-user.enum"; export * from "./event-type.enum"; export * from "./http-status-code.enum"; +export * from "./integration-type.enum"; export * from "./native-messaging-version.enum"; export * from "./notification-type.enum"; export * from "./product-type.enum"; diff --git a/libs/common/src/enums/integration-type.enum.ts b/libs/common/src/enums/integration-type.enum.ts new file mode 100644 index 0000000000..acb9510697 --- /dev/null +++ b/libs/common/src/enums/integration-type.enum.ts @@ -0,0 +1,4 @@ +export enum IntegrationType { + Integration = "integration", + SDK = "sdk", +} diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index 85b2bfe82e..6609a1014e 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -26,7 +26,7 @@ export abstract class CryptoService { * any other necessary versions (such as auto, biometrics, * or pin) * - * @throws when key is null. Use {@link clearUserKey} instead + * @throws when key is null. Lock the account to clear a key * @param key The user key to set * @param userId The desired user */ @@ -93,13 +93,6 @@ export abstract class CryptoService { * @returns A new user key and the master key protected version of it */ abstract makeUserKey(key: MasterKey): Promise<[UserKey, EncString]>; - /** - * Clears the user key - * @param clearStoredKeys Clears all stored versions of the user keys as well, - * such as the biometrics key - * @param userId The desired user - */ - abstract clearUserKey(clearSecretStorage?: boolean, userId?: string): Promise; /** * Clears the user's stored version of the user key * @param keySuffix The desired version of the key to clear @@ -112,18 +105,6 @@ export abstract class CryptoService { * @param userId The desired user */ abstract setMasterKeyEncryptedUserKey(UserKeyMasterKey: string, userId?: string): Promise; - /** - * Sets the user's master key - * @param key The user's master key to set - * @param userId The desired user - */ - abstract setMasterKey(key: MasterKey, userId?: string): Promise; - /** - * @param userId The desired user - * @returns The user's master key - */ - abstract getMasterKey(userId?: string): Promise; - /** * @param password The user's master password that will be used to derive a master key if one isn't found * @param userId The desired user @@ -143,11 +124,6 @@ export abstract class CryptoService { kdf: KdfType, KdfConfig: KdfConfig, ): Promise; - /** - * Clears the user's master key - * @param userId The desired user - */ - abstract clearMasterKey(userId?: string): Promise; /** * Encrypts the existing (or provided) user key with the * provided master key @@ -185,20 +161,6 @@ export abstract class CryptoService { key: MasterKey, hashPurpose?: HashPurpose, ): Promise; - /** - * Sets the user's master password hash - * @param keyHash The user's master password hash to set - */ - abstract setMasterKeyHash(keyHash: string): Promise; - /** - * @returns The user's master password hash - */ - abstract getMasterKeyHash(): Promise; - /** - * Clears the user's stored master password hash - * @param userId The desired user - */ - abstract clearMasterKeyHash(userId?: string): Promise; /** * Compares the provided master password to the stored password hash and server password hash. * Updates the stored hash if outdated. @@ -238,12 +200,6 @@ export abstract class CryptoService { abstract makeDataEncKey( key: T, ): Promise<[SymmetricCryptoKey, EncString]>; - /** - * Clears the user's stored organization keys - * @param memoryOnly Clear only the in-memory keys - * @param userId The desired user - */ - abstract clearOrgKeys(memoryOnly?: boolean, userId?: string): Promise; /** * Stores the encrypted provider keys and clears any decrypted * provider keys currently in memory @@ -260,11 +216,6 @@ export abstract class CryptoService { * @returns A record of the provider Ids to their symmetric keys */ abstract getProviderKeys(): Promise>; - /** - * @param memoryOnly Clear only the in-memory keys - * @param userId The desired user - */ - abstract clearProviderKeys(memoryOnly?: boolean, userId?: string): Promise; /** * Returns the public key from memory. If not available, extracts it * from the private key and stores it in memory @@ -304,12 +255,6 @@ export abstract class CryptoService { * @returns A new keypair: [publicKey in Base64, encrypted privateKey] */ abstract makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, EncString]>; - /** - * Clears the user's key pair - * @param memoryOnly Clear only the in-memory keys - * @param userId The desired user - */ - abstract clearKeyPair(memoryOnly?: boolean, userId?: string): Promise; /** * @param pin The user's pin * @param salt The user's salt diff --git a/libs/common/src/platform/abstractions/messaging.service.ts b/libs/common/src/platform/abstractions/messaging.service.ts index ab4332c283..f24279f932 100644 --- a/libs/common/src/platform/abstractions/messaging.service.ts +++ b/libs/common/src/platform/abstractions/messaging.service.ts @@ -1,3 +1,3 @@ -export abstract class MessagingService { - abstract send(subscriber: string, arg?: any): void; -} +// Export the new message sender as the legacy MessagingService to minimize changes in the initial PR, +// team specific PR's will come after. +export { MessageSender as MessagingService } from "../messaging/message.sender"; diff --git a/libs/common/src/platform/abstractions/platform-utils.service.ts b/libs/common/src/platform/abstractions/platform-utils.service.ts index d518a17f7b..f2dff46c78 100644 --- a/libs/common/src/platform/abstractions/platform-utils.service.ts +++ b/libs/common/src/platform/abstractions/platform-utils.service.ts @@ -28,6 +28,11 @@ export abstract class PlatformUtilsService { abstract getApplicationVersionNumber(): Promise; abstract supportsWebAuthn(win: Window): boolean; abstract supportsDuo(): boolean; + /** + * @deprecated use `@bitwarden/components/ToastService.showToast` instead + * + * Jira: [CL-213](https://bitwarden.atlassian.net/browse/CL-213) + */ abstract showToast( type: "error" | "success" | "warning" | "info", title: string, diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 4971481381..f1d4b3848e 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -1,23 +1,15 @@ import { Observable } from "rxjs"; -import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable"; -import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { BiometricKey } from "../../auth/types/biometric-key"; import { GeneratorOptions } from "../../tools/generator/generator-options"; import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; import { UsernameGeneratorOptions } from "../../tools/generator/username"; import { UserId } from "../../types/guid"; -import { MasterKey } from "../../types/key"; -import { CipherData } from "../../vault/models/data/cipher.data"; -import { LocalData } from "../../vault/models/data/local.data"; -import { CipherView } from "../../vault/models/view/cipher.view"; -import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info"; import { KdfType } from "../enums"; import { Account } from "../models/domain/account"; import { EncString } from "../models/domain/enc-string"; import { StorageOptions } from "../models/domain/storage-options"; -import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; /** * Options for customizing the initiation behavior. @@ -36,34 +28,12 @@ export type InitOptions = { export abstract class StateService { accounts$: Observable<{ [userId: string]: T }>; activeAccount$: Observable; - /** - * @deprecated use accountService.activeAccount$ instead - */ - activeAccountUnlocked$: Observable; addAccount: (account: T) => Promise; setActiveUser: (userId: string) => Promise; clean: (options?: StorageOptions) => Promise; init: (initOptions?: InitOptions) => Promise; - getAddEditCipherInfo: (options?: StorageOptions) => Promise; - setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise; - /** - * Gets the user's master key - */ - getMasterKey: (options?: StorageOptions) => Promise; - /** - * Sets the user's master key - */ - setMasterKey: (value: MasterKey, options?: StorageOptions) => Promise; - /** - * Gets the user key encrypted by the master key - */ - getMasterKeyEncryptedUserKey: (options?: StorageOptions) => Promise; - /** - * Sets the user key encrypted by the master key - */ - setMasterKeyEncryptedUserKey: (value: string, options?: StorageOptions) => Promise; /** * Gets the user's auto key */ @@ -104,14 +74,17 @@ export abstract class StateService { * Used when Lock with MP on Restart is enabled */ setPinKeyEncryptedUserKeyEphemeral: (value: EncString, options?: StorageOptions) => Promise; + /** + * @deprecated For backwards compatible purposes only, use DesktopAutofillSettingsService + */ + setEnableDuckDuckGoBrowserIntegration: ( + value: boolean, + options?: StorageOptions, + ) => Promise; /** * @deprecated For migration purposes only, use getUserKeyMasterKey instead */ getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise; - /** - * @deprecated For legacy purposes only, use getMasterKey instead - */ - getCryptoMasterKey: (options?: StorageOptions) => Promise; /** * @deprecated For migration purposes only, use getUserKeyAuto instead */ @@ -132,8 +105,6 @@ export abstract class StateService { * @deprecated For migration purposes only, use setUserKeyBiometric instead */ setCryptoMasterKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise; - getDecryptedCiphers: (options?: StorageOptions) => Promise; - setDecryptedCiphers: (value: CipherView[], options?: StorageOptions) => Promise; getDecryptedPasswordGenerationHistory: ( options?: StorageOptions, ) => Promise; @@ -151,11 +122,6 @@ export abstract class StateService { setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise; getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise; setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise; - getAdminAuthRequest: (options?: StorageOptions) => Promise; - setAdminAuthRequest: ( - adminAuthRequest: AdminAuthRequestStorable, - options?: StorageOptions, - ) => Promise; getEmail: (options?: StorageOptions) => Promise; setEmail: (value: string, options?: StorageOptions) => Promise; getEmailVerified: (options?: StorageOptions) => Promise; @@ -167,11 +133,6 @@ export abstract class StateService { value: boolean, options?: StorageOptions, ) => Promise; - getEncryptedCiphers: (options?: StorageOptions) => Promise<{ [id: string]: CipherData }>; - setEncryptedCiphers: ( - value: { [id: string]: CipherData }, - options?: StorageOptions, - ) => Promise; getEncryptedPasswordGenerationHistory: ( options?: StorageOptions, ) => Promise; @@ -187,29 +148,15 @@ export abstract class StateService { * @deprecated For migration purposes only, use setEncryptedUserKeyPin instead */ setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise; - getEverBeenUnlocked: (options?: StorageOptions) => Promise; - setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise; - getForceSetPasswordReason: (options?: StorageOptions) => Promise; - setForceSetPasswordReason: ( - value: ForceSetPasswordReason, - options?: StorageOptions, - ) => Promise; getIsAuthenticated: (options?: StorageOptions) => Promise; getKdfConfig: (options?: StorageOptions) => Promise; setKdfConfig: (kdfConfig: KdfConfig, options?: StorageOptions) => Promise; getKdfType: (options?: StorageOptions) => Promise; setKdfType: (value: KdfType, options?: StorageOptions) => Promise; - getKeyHash: (options?: StorageOptions) => Promise; - setKeyHash: (value: string, options?: StorageOptions) => Promise; getLastActive: (options?: StorageOptions) => Promise; setLastActive: (value: number, options?: StorageOptions) => Promise; getLastSync: (options?: StorageOptions) => Promise; setLastSync: (value: string, options?: StorageOptions) => Promise; - getLocalData: (options?: StorageOptions) => Promise<{ [cipherId: string]: LocalData }>; - setLocalData: ( - value: { [cipherId: string]: LocalData }, - options?: StorageOptions, - ) => Promise; getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise; setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise; getOrganizationInvitation: (options?: StorageOptions) => Promise; @@ -234,14 +181,10 @@ export abstract class StateService { * Sets the user's Pin, encrypted by the user key */ setProtectedPin: (value: string, options?: StorageOptions) => Promise; - getSecurityStamp: (options?: StorageOptions) => Promise; - setSecurityStamp: (value: string, options?: StorageOptions) => Promise; getUserId: (options?: StorageOptions) => Promise; getVaultTimeout: (options?: StorageOptions) => Promise; setVaultTimeout: (value: number, options?: StorageOptions) => Promise; getVaultTimeoutAction: (options?: StorageOptions) => Promise; setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise; - getApproveLoginRequests: (options?: StorageOptions) => Promise; - setApproveLoginRequests: (value: boolean, options?: StorageOptions) => Promise; nextUpActiveUser: () => Promise; } diff --git a/libs/common/src/platform/biometrics/biometric.state.spec.ts b/libs/common/src/platform/biometrics/biometric.state.spec.ts index 420a0fb86e..7bcccd2ea9 100644 --- a/libs/common/src/platform/biometrics/biometric.state.spec.ts +++ b/libs/common/src/platform/biometrics/biometric.state.spec.ts @@ -1,5 +1,5 @@ import { EncryptedString } from "../models/domain/enc-string"; -import { KeyDefinition } from "../state"; +import { KeyDefinition, UserKeyDefinition } from "../state"; import { BIOMETRIC_UNLOCK_ENABLED, @@ -22,9 +22,15 @@ describe.each([ ])( "deserializes state %s", ( - ...args: [KeyDefinition, EncryptedString] | [KeyDefinition, boolean] + ...args: + | [UserKeyDefinition, EncryptedString] + | [UserKeyDefinition, boolean] + | [KeyDefinition, boolean] ) => { - function testDeserialization(keyDefinition: KeyDefinition, state: T) { + function testDeserialization( + keyDefinition: UserKeyDefinition | KeyDefinition, + state: T, + ) { const deserialized = keyDefinition.deserializer(JSON.parse(JSON.stringify(state))); expect(deserialized).toEqual(state); } diff --git a/libs/common/src/platform/biometrics/biometric.state.ts b/libs/common/src/platform/biometrics/biometric.state.ts index aa16e14baa..bcefb7b215 100644 --- a/libs/common/src/platform/biometrics/biometric.state.ts +++ b/libs/common/src/platform/biometrics/biometric.state.ts @@ -1,15 +1,16 @@ import { UserId } from "../../types/guid"; import { EncryptedString } from "../models/domain/enc-string"; -import { KeyDefinition, BIOMETRIC_SETTINGS_DISK } from "../state"; +import { KeyDefinition, BIOMETRIC_SETTINGS_DISK, UserKeyDefinition } from "../state"; /** * Indicates whether the user elected to store a biometric key to unlock their vault. */ -export const BIOMETRIC_UNLOCK_ENABLED = new KeyDefinition( +export const BIOMETRIC_UNLOCK_ENABLED = new UserKeyDefinition( BIOMETRIC_SETTINGS_DISK, "biometricUnlockEnabled", { deserializer: (obj) => obj, + clearOn: [], }, ); @@ -18,11 +19,12 @@ export const BIOMETRIC_UNLOCK_ENABLED = new KeyDefinition( * * A true setting controls whether {@link ENCRYPTED_CLIENT_KEY_HALF} is set. */ -export const REQUIRE_PASSWORD_ON_START = new KeyDefinition( +export const REQUIRE_PASSWORD_ON_START = new UserKeyDefinition( BIOMETRIC_SETTINGS_DISK, "requirePasswordOnStart", { deserializer: (value) => value, + clearOn: [], }, ); @@ -33,11 +35,12 @@ export const REQUIRE_PASSWORD_ON_START = new KeyDefinition( * For operating systems without application-level key storage, this key half is concatenated with a signature * provided by the OS and used to encrypt the biometric key prior to storage. */ -export const ENCRYPTED_CLIENT_KEY_HALF = new KeyDefinition( +export const ENCRYPTED_CLIENT_KEY_HALF = new UserKeyDefinition( BIOMETRIC_SETTINGS_DISK, "clientKeyHalf", { deserializer: (obj) => obj, + clearOn: ["logout"], }, ); @@ -45,11 +48,12 @@ export const ENCRYPTED_CLIENT_KEY_HALF = new KeyDefinition( * Indicates the user has been warned about the security implications of using biometrics and, depending on the OS, * recommended to require a password on first unlock of an application instance. */ -export const DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT = new KeyDefinition( +export const DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT = new UserKeyDefinition( BIOMETRIC_SETTINGS_DISK, "dismissedBiometricRequirePasswordOnStartCallout", { deserializer: (obj) => obj, + clearOn: [], }, ); @@ -68,11 +72,12 @@ export const PROMPT_CANCELLED = KeyDefinition.record( /** * Stores whether the user has elected to automatically prompt for biometric unlock on application start. */ -export const PROMPT_AUTOMATICALLY = new KeyDefinition( +export const PROMPT_AUTOMATICALLY = new UserKeyDefinition( BIOMETRIC_SETTINGS_DISK, "promptAutomatically", { deserializer: (obj) => obj, + clearOn: [], }, ); diff --git a/libs/common/src/platform/messaging/helpers.spec.ts b/libs/common/src/platform/messaging/helpers.spec.ts new file mode 100644 index 0000000000..fcd36b4411 --- /dev/null +++ b/libs/common/src/platform/messaging/helpers.spec.ts @@ -0,0 +1,46 @@ +import { Subject, firstValueFrom } from "rxjs"; + +import { getCommand, isExternalMessage, tagAsExternal } from "./helpers"; +import { Message, CommandDefinition } from "./types"; + +describe("helpers", () => { + describe("getCommand", () => { + it("can get the command from just a string", () => { + const command = getCommand("myCommand"); + + expect(command).toEqual("myCommand"); + }); + + it("can get the command from a message definition", () => { + const commandDefinition = new CommandDefinition("myCommand"); + + const command = getCommand(commandDefinition); + + expect(command).toEqual("myCommand"); + }); + }); + + describe("tag integration", () => { + it("can tag and identify as tagged", async () => { + const messagesSubject = new Subject>(); + + const taggedMessages = messagesSubject.asObservable().pipe(tagAsExternal); + + const firstValuePromise = firstValueFrom(taggedMessages); + + messagesSubject.next({ command: "test" }); + + const result = await firstValuePromise; + + expect(isExternalMessage(result)).toEqual(true); + }); + }); + + describe("isExternalMessage", () => { + it.each([null, { command: "myCommand", test: "object" }, undefined] as Message< + Record + >[])("returns false when value is %s", (value: Message) => { + expect(isExternalMessage(value)).toBe(false); + }); + }); +}); diff --git a/libs/common/src/platform/messaging/helpers.ts b/libs/common/src/platform/messaging/helpers.ts new file mode 100644 index 0000000000..bf119432e0 --- /dev/null +++ b/libs/common/src/platform/messaging/helpers.ts @@ -0,0 +1,23 @@ +import { MonoTypeOperatorFunction, map } from "rxjs"; + +import { Message, CommandDefinition } from "./types"; + +export const getCommand = (commandDefinition: CommandDefinition | string) => { + if (typeof commandDefinition === "string") { + return commandDefinition; + } else { + return commandDefinition.command; + } +}; + +export const EXTERNAL_SOURCE_TAG = Symbol("externalSource"); + +export const isExternalMessage = (message: Message) => { + return (message as Record)?.[EXTERNAL_SOURCE_TAG] === true; +}; + +export const tagAsExternal: MonoTypeOperatorFunction> = map( + (message: Message) => { + return Object.assign(message, { [EXTERNAL_SOURCE_TAG]: true }); + }, +); diff --git a/libs/common/src/platform/messaging/index.ts b/libs/common/src/platform/messaging/index.ts new file mode 100644 index 0000000000..a9b4eca5ae --- /dev/null +++ b/libs/common/src/platform/messaging/index.ts @@ -0,0 +1,4 @@ +export { MessageListener } from "./message.listener"; +export { MessageSender } from "./message.sender"; +export { Message, CommandDefinition } from "./types"; +export { isExternalMessage } from "./helpers"; diff --git a/libs/common/src/platform/messaging/internal.ts b/libs/common/src/platform/messaging/internal.ts new file mode 100644 index 0000000000..08763d48bc --- /dev/null +++ b/libs/common/src/platform/messaging/internal.ts @@ -0,0 +1,5 @@ +// Built in implementations +export { SubjectMessageSender } from "./subject-message.sender"; + +// Helpers meant to be used only by other implementations +export { tagAsExternal, getCommand } from "./helpers"; diff --git a/libs/common/src/platform/messaging/message.listener.spec.ts b/libs/common/src/platform/messaging/message.listener.spec.ts new file mode 100644 index 0000000000..98bbf1fdc8 --- /dev/null +++ b/libs/common/src/platform/messaging/message.listener.spec.ts @@ -0,0 +1,47 @@ +import { Subject } from "rxjs"; + +import { subscribeTo } from "../../../spec/observable-tracker"; + +import { MessageListener } from "./message.listener"; +import { Message, CommandDefinition } from "./types"; + +describe("MessageListener", () => { + const subject = new Subject>(); + const sut = new MessageListener(subject.asObservable()); + + const testCommandDefinition = new CommandDefinition<{ test: number }>("myCommand"); + + describe("allMessages$", () => { + it("runs on all nexts", async () => { + const tracker = subscribeTo(sut.allMessages$); + + const pausePromise = tracker.pauseUntilReceived(2); + + subject.next({ command: "command1", test: 1 }); + subject.next({ command: "command2", test: 2 }); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "command1", test: 1 }); + expect(tracker.emissions[1]).toEqual({ command: "command2", test: 2 }); + }); + }); + + describe("messages$", () => { + it("runs on only my commands", async () => { + const tracker = subscribeTo(sut.messages$(testCommandDefinition)); + + const pausePromise = tracker.pauseUntilReceived(2); + + subject.next({ command: "notMyCommand", test: 1 }); + subject.next({ command: "myCommand", test: 2 }); + subject.next({ command: "myCommand", test: 3 }); + subject.next({ command: "notMyCommand", test: 4 }); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 2 }); + expect(tracker.emissions[1]).toEqual({ command: "myCommand", test: 3 }); + }); + }); +}); diff --git a/libs/common/src/platform/messaging/message.listener.ts b/libs/common/src/platform/messaging/message.listener.ts new file mode 100644 index 0000000000..df453c8422 --- /dev/null +++ b/libs/common/src/platform/messaging/message.listener.ts @@ -0,0 +1,41 @@ +import { EMPTY, Observable, filter } from "rxjs"; + +import { Message, CommandDefinition } from "./types"; + +/** + * A class that allows for listening to messages coming through the application, + * allows for listening of all messages or just the messages you care about. + * + * @note Consider NOT using messaging at all if you can. State Providers offer an observable stream of + * data that is persisted. This can serve messages that might have been used to notify of settings changes + * or vault data changes and those observables should be preferred over messaging. + */ +export class MessageListener { + constructor(private readonly messageStream: Observable>) {} + + /** + * A stream of all messages sent through the application. It does not contain type information for the + * other properties on the messages. You are encouraged to instead subscribe to an individual message + * through {@link messages$}. + */ + allMessages$ = this.messageStream; + + /** + * Creates an observable stream filtered to just the command given via the {@link CommandDefinition} and typed + * to the generic contained in the CommandDefinition. Be careful using this method unless all your messages are being + * sent through `MessageSender.send`, if that isn't the case you should have lower confidence in the message + * payload being the expected type. + * + * @param commandDefinition The CommandDefinition containing the information about the message type you care about. + */ + messages$(commandDefinition: CommandDefinition): Observable { + return this.allMessages$.pipe( + filter((msg) => msg?.command === commandDefinition.command), + ) as Observable; + } + + /** + * A helper property for returning a MessageListener that will never emit any messages and will immediately complete. + */ + static readonly EMPTY = new MessageListener(EMPTY); +} diff --git a/libs/common/src/platform/messaging/message.sender.ts b/libs/common/src/platform/messaging/message.sender.ts new file mode 100644 index 0000000000..6bf2661580 --- /dev/null +++ b/libs/common/src/platform/messaging/message.sender.ts @@ -0,0 +1,62 @@ +import { CommandDefinition } from "./types"; + +class MultiMessageSender implements MessageSender { + constructor(private readonly innerMessageSenders: MessageSender[]) {} + + send( + commandDefinition: string | CommandDefinition, + payload: object | T = {}, + ): void { + for (const messageSender of this.innerMessageSenders) { + messageSender.send(commandDefinition, payload); + } + } +} + +export abstract class MessageSender { + /** + * A method for sending messages in a type safe manner. The passed in command definition + * will require you to provide a compatible type in the payload parameter. + * + * @example + * const MY_COMMAND = new CommandDefinition<{ test: number }>("myCommand"); + * + * this.messageSender.send(MY_COMMAND, { test: 14 }); + * + * @param commandDefinition + * @param payload + */ + abstract send(commandDefinition: CommandDefinition, payload: T): void; + + /** + * A legacy method for sending messages in a non-type safe way. + * + * @remarks Consider defining a {@link CommandDefinition} and passing that in for the first parameter to + * get compilation errors when defining an incompatible payload. + * + * @param command The string based command of your message. + * @param payload Extra contextual information regarding the message. Be aware that this payload may + * be serialized and lose all prototype information. + */ + abstract send(command: string, payload?: object): void; + + /** Implementation of the other two overloads, read their docs instead. */ + abstract send( + commandDefinition: CommandDefinition | string, + payload: T | object, + ): void; + + /** + * A helper method for combine multiple {@link MessageSender}'s. + * @param messageSenders The message senders that should be combined. + * @returns A message sender that will relay all messages to the given message senders. + */ + static combine(...messageSenders: MessageSender[]) { + return new MultiMessageSender(messageSenders); + } + + /** + * A helper property for creating a {@link MessageSender} that sends to nowhere. + */ + static readonly EMPTY: MessageSender = new MultiMessageSender([]); +} diff --git a/libs/common/src/platform/messaging/subject-message.sender.spec.ts b/libs/common/src/platform/messaging/subject-message.sender.spec.ts new file mode 100644 index 0000000000..4278fca7bc --- /dev/null +++ b/libs/common/src/platform/messaging/subject-message.sender.spec.ts @@ -0,0 +1,65 @@ +import { Subject } from "rxjs"; + +import { subscribeTo } from "../../../spec/observable-tracker"; + +import { SubjectMessageSender } from "./internal"; +import { MessageSender } from "./message.sender"; +import { Message, CommandDefinition } from "./types"; + +describe("SubjectMessageSender", () => { + const subject = new Subject>(); + const subjectObservable = subject.asObservable(); + + const sut: MessageSender = new SubjectMessageSender(subject); + + describe("send", () => { + it("will send message with command from message definition", async () => { + const commandDefinition = new CommandDefinition<{ test: number }>("myCommand"); + + const tracker = subscribeTo(subjectObservable); + const pausePromise = tracker.pauseUntilReceived(1); + + sut.send(commandDefinition, { test: 1 }); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 1 }); + }); + + it("will send message with command from normal string", async () => { + const tracker = subscribeTo(subjectObservable); + const pausePromise = tracker.pauseUntilReceived(1); + + sut.send("myCommand", { test: 1 }); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 1 }); + }); + + it("will send message with object even if payload not given", async () => { + const tracker = subscribeTo(subjectObservable); + const pausePromise = tracker.pauseUntilReceived(1); + + sut.send("myCommand"); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "myCommand" }); + }); + + it.each([null, undefined])( + "will send message with object even if payload is null-ish (%s)", + async (payloadValue) => { + const tracker = subscribeTo(subjectObservable); + const pausePromise = tracker.pauseUntilReceived(1); + + sut.send("myCommand", payloadValue); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "myCommand" }); + }, + ); + }); +}); diff --git a/libs/common/src/platform/messaging/subject-message.sender.ts b/libs/common/src/platform/messaging/subject-message.sender.ts new file mode 100644 index 0000000000..94ae6f27f3 --- /dev/null +++ b/libs/common/src/platform/messaging/subject-message.sender.ts @@ -0,0 +1,17 @@ +import { Subject } from "rxjs"; + +import { getCommand } from "./internal"; +import { MessageSender } from "./message.sender"; +import { Message, CommandDefinition } from "./types"; + +export class SubjectMessageSender implements MessageSender { + constructor(private readonly messagesSubject: Subject>) {} + + send( + commandDefinition: string | CommandDefinition, + payload: object | T = {}, + ): void { + const command = getCommand(commandDefinition); + this.messagesSubject.next(Object.assign(payload ?? {}, { command: command })); + } +} diff --git a/libs/common/src/platform/messaging/types.ts b/libs/common/src/platform/messaging/types.ts new file mode 100644 index 0000000000..f30163344f --- /dev/null +++ b/libs/common/src/platform/messaging/types.ts @@ -0,0 +1,13 @@ +declare const tag: unique symbol; + +/** + * A class for defining information about a message, this is helpful + * alonside `MessageSender` and `MessageListener` for providing a type + * safe(-ish) way of sending and receiving messages. + */ +export class CommandDefinition { + [tag]: T; + constructor(readonly command: string) {} +} + +export type Message = { command: string } & T; diff --git a/libs/common/src/platform/misc/flags.ts b/libs/common/src/platform/misc/flags.ts index c1f8d7757b..cc463b1060 100644 --- a/libs/common/src/platform/misc/flags.ts +++ b/libs/common/src/platform/misc/flags.ts @@ -1,5 +1,5 @@ // required to avoid linting errors when there are no flags -/* eslint-disable @typescript-eslint/ban-types */ +// eslint-disable-next-line @typescript-eslint/ban-types export type SharedFlags = { multithreadDecryption: boolean; showPasswordless?: boolean; @@ -7,7 +7,7 @@ export type SharedFlags = { }; // required to avoid linting errors when there are no flags -/* eslint-disable @typescript-eslint/ban-types */ +// eslint-disable-next-line @typescript-eslint/ban-types export type SharedDevFlags = { noopNotifications: boolean; }; diff --git a/libs/common/src/platform/models/domain/account-keys.spec.ts b/libs/common/src/platform/models/domain/account-keys.spec.ts index 4a96da1b48..6bdb08edd5 100644 --- a/libs/common/src/platform/models/domain/account-keys.spec.ts +++ b/libs/common/src/platform/models/domain/account-keys.spec.ts @@ -2,7 +2,6 @@ import { makeStaticByteArray } from "../../../../spec"; import { Utils } from "../../misc/utils"; import { AccountKeys, EncryptionPair } from "./account"; -import { SymmetricCryptoKey } from "./symmetric-crypto-key"; describe("AccountKeys", () => { describe("toJSON", () => { @@ -32,12 +31,6 @@ describe("AccountKeys", () => { expect(keys.publicKey).toEqual(Utils.fromByteStringToArray("hello")); }); - it("should deserialize cryptoMasterKey", () => { - const spy = jest.spyOn(SymmetricCryptoKey, "fromJSON"); - AccountKeys.fromJSON({} as any); - expect(spy).toHaveBeenCalled(); - }); - it("should deserialize privateKey", () => { const spy = jest.spyOn(EncryptionPair, "fromJSON"); AccountKeys.fromJSON({ diff --git a/libs/common/src/platform/models/domain/account-tokens.spec.ts b/libs/common/src/platform/models/domain/account-tokens.spec.ts deleted file mode 100644 index 733b3908e9..0000000000 --- a/libs/common/src/platform/models/domain/account-tokens.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AccountTokens } from "./account"; - -describe("AccountTokens", () => { - describe("fromJSON", () => { - it("should deserialize to an instance of itself", () => { - expect(AccountTokens.fromJSON({})).toBeInstanceOf(AccountTokens); - }); - }); -}); diff --git a/libs/common/src/platform/models/domain/account.spec.ts b/libs/common/src/platform/models/domain/account.spec.ts index 0c76c16cc2..77c242b6ff 100644 --- a/libs/common/src/platform/models/domain/account.spec.ts +++ b/libs/common/src/platform/models/domain/account.spec.ts @@ -1,4 +1,4 @@ -import { Account, AccountKeys, AccountProfile, AccountSettings, AccountTokens } from "./account"; +import { Account, AccountKeys, AccountProfile, AccountSettings } from "./account"; describe("Account", () => { describe("fromJSON", () => { @@ -10,14 +10,12 @@ describe("Account", () => { const keysSpy = jest.spyOn(AccountKeys, "fromJSON"); const profileSpy = jest.spyOn(AccountProfile, "fromJSON"); const settingsSpy = jest.spyOn(AccountSettings, "fromJSON"); - const tokensSpy = jest.spyOn(AccountTokens, "fromJSON"); Account.fromJSON({}); expect(keysSpy).toHaveBeenCalled(); expect(profileSpy).toHaveBeenCalled(); expect(settingsSpy).toHaveBeenCalled(); - expect(tokensSpy).toHaveBeenCalled(); }); }); }); diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 4ed36fd389..cd416ec1f9 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -1,7 +1,5 @@ import { Jsonify } from "type-fest"; -import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable"; -import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { GeneratorOptions } from "../../../tools/generator/generator-options"; import { @@ -10,10 +8,6 @@ import { } from "../../../tools/generator/password"; import { UsernameGeneratorOptions } from "../../../tools/generator/username/username-generation-options"; import { DeepJsonify } from "../../../types/deep-jsonify"; -import { MasterKey } from "../../../types/key"; -import { CipherData } from "../../../vault/models/data/cipher.data"; -import { CipherView } from "../../../vault/models/view/cipher.view"; -import { AddEditCipherInfo } from "../../../vault/types/add-edit-cipher-info"; import { KdfType } from "../../enums"; import { Utils } from "../../misc/utils"; @@ -64,38 +58,23 @@ export class DataEncryptionPair { } export class AccountData { - ciphers?: DataEncryptionPair = new DataEncryptionPair< - CipherData, - CipherView - >(); - localData?: any; passwordGenerationHistory?: EncryptionPair< GeneratedPasswordHistory[], GeneratedPasswordHistory[] > = new EncryptionPair(); - addEditCipherInfo?: AddEditCipherInfo; static fromJSON(obj: DeepJsonify): AccountData { if (obj == null) { return null; } - return Object.assign(new AccountData(), obj, { - addEditCipherInfo: { - cipher: CipherView.fromJSON(obj?.addEditCipherInfo?.cipher), - collectionIds: obj?.addEditCipherInfo?.collectionIds, - }, - }); + return Object.assign(new AccountData(), obj); } } export class AccountKeys { - masterKey?: MasterKey; - masterKeyEncryptedUserKey?: string; publicKey?: Uint8Array; - /** @deprecated July 2023, left for migration purposes*/ - cryptoMasterKey?: SymmetricCryptoKey; /** @deprecated July 2023, left for migration purposes*/ cryptoMasterKeyAuto?: string; /** @deprecated July 2023, left for migration purposes*/ @@ -120,8 +99,6 @@ export class AccountKeys { return null; } return Object.assign(new AccountKeys(), obj, { - masterKey: SymmetricCryptoKey.fromJSON(obj?.masterKey), - cryptoMasterKey: SymmetricCryptoKey.fromJSON(obj?.cryptoMasterKey), cryptoSymmetricKey: EncryptionPair.fromJSON( obj?.cryptoSymmetricKey, SymmetricCryptoKey.fromJSON, @@ -149,11 +126,8 @@ export class AccountProfile { name?: string; email?: string; emailVerified?: boolean; - everBeenUnlocked?: boolean; - forceSetPasswordReason?: ForceSetPasswordReason; lastSync?: string; userId?: string; - keyHash?: string; kdfIterations?: number; kdfMemory?: number; kdfParallelism?: number; @@ -179,7 +153,6 @@ export class AccountSettings { protectedPin?: string; vaultTimeout?: number; vaultTimeoutAction?: string = "lock"; - approveLoginRequests?: boolean; /** @deprecated July 2023, left for migration purposes*/ pinProtected?: EncryptionPair = new EncryptionPair(); @@ -198,25 +171,11 @@ export class AccountSettings { } } -export class AccountTokens { - securityStamp?: string; - - static fromJSON(obj: Jsonify): AccountTokens { - if (obj == null) { - return null; - } - - return Object.assign(new AccountTokens(), obj); - } -} - export class Account { data?: AccountData = new AccountData(); keys?: AccountKeys = new AccountKeys(); profile?: AccountProfile = new AccountProfile(); settings?: AccountSettings = new AccountSettings(); - tokens?: AccountTokens = new AccountTokens(); - adminAuthRequest?: Jsonify = null; constructor(init: Partial) { Object.assign(this, { @@ -236,11 +195,6 @@ export class Account { ...new AccountSettings(), ...init?.settings, }, - tokens: { - ...new AccountTokens(), - ...init?.tokens, - }, - adminAuthRequest: init?.adminAuthRequest, }); } @@ -254,8 +208,6 @@ export class Account { data: AccountData.fromJSON(json?.data), profile: AccountProfile.fromJSON(json?.profile), settings: AccountSettings.fromJSON(json?.settings), - tokens: AccountTokens.fromJSON(json?.tokens), - adminAuthRequest: AdminAuthRequestStorable.fromJSON(json?.adminAuthRequest), }); } } diff --git a/libs/common/src/platform/models/domain/global-state.ts b/libs/common/src/platform/models/domain/global-state.ts index 703a998d1c..cd7cf7d174 100644 --- a/libs/common/src/platform/models/domain/global-state.ts +++ b/libs/common/src/platform/models/domain/global-state.ts @@ -4,4 +4,5 @@ export class GlobalState { vaultTimeoutAction?: string; enableBrowserIntegration?: boolean; enableBrowserIntegrationFingerprint?: boolean; + enableDuckDuckGoBrowserIntegration?: boolean; } diff --git a/libs/common/src/platform/services/broadcaster.service.ts b/libs/common/src/platform/services/broadcaster.service.ts deleted file mode 100644 index 9d823b00e0..0000000000 --- a/libs/common/src/platform/services/broadcaster.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - BroadcasterService as BroadcasterServiceAbstraction, - MessageBase, -} from "../abstractions/broadcaster.service"; - -export class BroadcasterService implements BroadcasterServiceAbstraction { - subscribers: Map void> = new Map< - string, - (message: MessageBase) => void - >(); - - send(message: MessageBase, id?: string) { - if (id != null) { - if (this.subscribers.has(id)) { - this.subscribers.get(id)(message); - } - return; - } - - this.subscribers.forEach((value) => { - value(message); - }); - } - - subscribe(id: string, messageCallback: (message: MessageBase) => void) { - this.subscribers.set(id, messageCallback); - } - - unsubscribe(id: string) { - if (this.subscribers.has(id)) { - this.subscribers.delete(id); - } - } -} diff --git a/libs/common/src/platform/services/config/default-config.service.ts b/libs/common/src/platform/services/config/default-config.service.ts index 9532b903d3..e124deccf8 100644 --- a/libs/common/src/platform/services/config/default-config.service.ts +++ b/libs/common/src/platform/services/config/default-config.service.ts @@ -32,7 +32,6 @@ export const USER_SERVER_CONFIG = new UserKeyDefinition(CONFIG_DIS clearOn: ["logout"], }); -// TODO MDG: When to clean these up? export const GLOBAL_SERVER_CONFIGURATIONS = KeyDefinition.record( CONFIG_DISK, "byServer", diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/common/src/platform/services/crypto.service.spec.ts index 9160664aa5..2f68cf2ce7 100644 --- a/libs/common/src/platform/services/crypto.service.spec.ts +++ b/libs/common/src/platform/services/crypto.service.spec.ts @@ -1,10 +1,10 @@ import { mock } from "jest-mock-extended"; -import { firstValueFrom, of } from "rxjs"; +import { firstValueFrom, of, tap } from "rxjs"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state"; import { FakeStateProvider } from "../../../spec/fake-state-provider"; -import { AuthenticationStatus } from "../../auth/enums/authentication-status"; +import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service"; import { CsprngArray } from "../../types/csprng"; import { UserId } from "../../types/guid"; import { UserKey, MasterKey, PinKey } from "../../types/key"; @@ -18,6 +18,7 @@ import { Utils } from "../misc/utils"; import { EncString } from "../models/domain/enc-string"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; import { CryptoService } from "../services/crypto.service"; +import { UserKeyDefinition } from "../state"; import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "./key-state/org-keys.state"; import { USER_ENCRYPTED_PROVIDER_KEYS } from "./key-state/provider-keys.state"; @@ -40,12 +41,15 @@ describe("cryptoService", () => { const mockUserId = Utils.newGuid() as UserId; let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; beforeEach(() => { accountService = mockAccountServiceWith(mockUserId); + masterPasswordService = new FakeMasterPasswordService(); stateProvider = new FakeStateProvider(accountService); cryptoService = new CryptoService( + masterPasswordService, keyGenerationService, cryptoFunctionService, encryptService, @@ -87,21 +91,7 @@ describe("cryptoService", () => { expect(userKey).toEqual(mockUserKey); }); - it("sets from the Auto key if the User Key if not set", async () => { - const autoKeyB64 = - "IT5cA1i5Hncd953pb00E58D2FqJX+fWTj4AvoI67qkGHSQPgulAqKv+LaKRAo9Bg0xzP9Nw00wk4TqjMmGSM+g=="; - stateService.getUserKeyAutoUnlock.mockResolvedValue(autoKeyB64); - const setKeySpy = jest.spyOn(cryptoService, "setUserKey"); - - const userKey = await cryptoService.getUserKey(mockUserId); - - expect(setKeySpy).toHaveBeenCalledWith(expect.any(SymmetricCryptoKey), mockUserId); - expect(setKeySpy).toHaveBeenCalledTimes(1); - - expect(userKey.keyB64).toEqual(autoKeyB64); - }); - - it("returns nullish if there is no auto key and the user key is not set", async () => { + it("returns nullish if the user key is not set", async () => { const userKey = await cryptoService.getUserKey(mockUserId); expect(userKey).toBeFalsy(); @@ -143,28 +133,17 @@ describe("cryptoService", () => { }, ); - describe("hasUserKey", () => { - it.each([true, false])( - "returns %s when the user key is not in memory, but the auto key is set", - async (hasKey) => { - stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(null); - cryptoService.hasUserKeyStored = jest.fn().mockResolvedValue(hasKey); - expect(await cryptoService.hasUserKey(mockUserId)).toBe(hasKey); - }, - ); - }); - describe("getUserKeyWithLegacySupport", () => { let mockUserKey: UserKey; let mockMasterKey: MasterKey; - let stateSvcGetMasterKey: jest.SpyInstance; + let getMasterKey: jest.SpyInstance; beforeEach(() => { const mockRandomBytes = new Uint8Array(64) as CsprngArray; mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey; - stateSvcGetMasterKey = jest.spyOn(stateService, "getMasterKey"); + getMasterKey = jest.spyOn(masterPasswordService, "masterKey$"); }); it("returns the User Key if available", async () => { @@ -174,17 +153,17 @@ describe("cryptoService", () => { const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId); expect(getKeySpy).toHaveBeenCalledWith(mockUserId); - expect(stateSvcGetMasterKey).not.toHaveBeenCalled(); + expect(getMasterKey).not.toHaveBeenCalled(); expect(userKey).toEqual(mockUserKey); }); it("returns the user's master key when User Key is not available", async () => { - stateSvcGetMasterKey.mockResolvedValue(mockMasterKey); + masterPasswordService.masterKeySubject.next(mockMasterKey); const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId); - expect(stateSvcGetMasterKey).toHaveBeenCalledWith({ userId: mockUserId }); + expect(getMasterKey).toHaveBeenCalledWith(mockUserId); expect(userKey).toEqual(mockMasterKey); }); }); @@ -268,15 +247,6 @@ describe("cryptoService", () => { await expect(cryptoService.setUserKey(null, mockUserId)).rejects.toThrow("No key provided."); }); - it("should update the user's lock state", async () => { - await cryptoService.setUserKey(mockUserKey, mockUserId); - - expect(accountService.mock.setAccountStatus).toHaveBeenCalledWith( - mockUserId, - AuthenticationStatus.Unlocked, - ); - }); - describe("Pin Key refresh", () => { let cryptoSvcMakePinKey: jest.SpyInstance; const protectedPin = @@ -336,249 +306,40 @@ describe("cryptoService", () => { }); }); - describe("clearUserKey", () => { - it.each([mockUserId, null])("should clear the User Key for id %2", async (userId) => { - await cryptoService.clearUserKey(false, userId); + describe("clearKeys", () => { + it("resolves active user id when called with no user id", async () => { + let callCount = 0; + stateProvider.activeUserId$ = stateProvider.activeUserId$.pipe(tap(() => callCount++)); - expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(USER_KEY, null, userId); + await cryptoService.clearKeys(null); + expect(callCount).toBe(1); + + // revert to the original state + accountService.activeAccount$ = accountService.activeAccountSubject.asObservable(); }); - it("should update status to locked", async () => { - await cryptoService.clearUserKey(false, mockUserId); + describe.each([ + USER_ENCRYPTED_ORGANIZATION_KEYS, + USER_ENCRYPTED_PROVIDER_KEYS, + USER_ENCRYPTED_PRIVATE_KEY, + USER_KEY, + ])("key removal", (key: UserKeyDefinition) => { + it(`clears ${key.key} for active user when unspecified`, async () => { + await cryptoService.clearKeys(null); - expect(accountService.mock.setMaxAccountStatus).toHaveBeenCalledWith( - mockUserId, - AuthenticationStatus.Locked, - ); - }); + const encryptedOrgKeyState = stateProvider.singleUser.getFake(mockUserId, key); + expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledTimes(1); + expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledWith(null); + }); - it.each([true, false])( - "should clear stored user keys if clearAll is true (%s)", - async (clear) => { - const clearSpy = (cryptoService["clearAllStoredUserKeys"] = jest.fn()); - await cryptoService.clearUserKey(clear, mockUserId); + it(`clears ${key.key} for the specified user when specified`, async () => { + const userId = "someOtherUser" as UserId; + await cryptoService.clearKeys(userId); - if (clear) { - expect(clearSpy).toHaveBeenCalledWith(mockUserId); - expect(clearSpy).toHaveBeenCalledTimes(1); - } else { - expect(clearSpy).not.toHaveBeenCalled(); - } - }, - ); - }); - - describe("clearOrgKeys", () => { - let forceMemorySpy: jest.Mock; - beforeEach(() => { - forceMemorySpy = cryptoService["activeUserOrgKeysState"].forceValue = jest.fn(); - }); - it("clears in memory org keys when called with memoryOnly", async () => { - await cryptoService.clearOrgKeys(true); - - expect(forceMemorySpy).toHaveBeenCalledWith({}); - }); - - it("does not clear memory when called with the non active user and memory only", async () => { - await cryptoService.clearOrgKeys(true, "someOtherUser" as UserId); - - expect(forceMemorySpy).not.toHaveBeenCalled(); - }); - - it("does not write to disk state if called with memory only", async () => { - await cryptoService.clearOrgKeys(true); - - expect(stateProvider.singleUser.mock.get).not.toHaveBeenCalled(); - }); - - it("clears disk state when called with diskOnly", async () => { - await cryptoService.clearOrgKeys(false); - - expect(stateProvider.singleUser.mock.get).toHaveBeenCalledWith( - mockUserId, - USER_ENCRYPTED_ORGANIZATION_KEYS, - ); - expect( - stateProvider.singleUser.getFake(mockUserId, USER_ENCRYPTED_ORGANIZATION_KEYS).nextMock, - ).toHaveBeenCalledWith(null); - }); - - it("clears another user's disk state when called with diskOnly and that user", async () => { - await cryptoService.clearOrgKeys(false, "someOtherUser" as UserId); - - expect(stateProvider.singleUser.mock.get).toHaveBeenCalledWith( - "someOtherUser" as UserId, - USER_ENCRYPTED_ORGANIZATION_KEYS, - ); - expect( - stateProvider.singleUser.getFake( - "someOtherUser" as UserId, - USER_ENCRYPTED_ORGANIZATION_KEYS, - ).nextMock, - ).toHaveBeenCalledWith(null); - }); - - it("does not clear active user disk state when called with diskOnly and a different specified user", async () => { - await cryptoService.clearOrgKeys(false, "someOtherUser" as UserId); - - expect(stateProvider.singleUser.mock.get).not.toHaveBeenCalledWith( - mockUserId, - USER_ENCRYPTED_ORGANIZATION_KEYS, - ); - }); - }); - - describe("clearProviderKeys", () => { - let forceMemorySpy: jest.Mock; - beforeEach(() => { - forceMemorySpy = cryptoService["activeUserProviderKeysState"].forceValue = jest.fn(); - }); - it("clears in memory org keys when called with memoryOnly", async () => { - await cryptoService.clearProviderKeys(true); - - expect(forceMemorySpy).toHaveBeenCalledWith({}); - }); - - it("does not clear memory when called with the non active user and memory only", async () => { - await cryptoService.clearProviderKeys(true, "someOtherUser" as UserId); - - expect(forceMemorySpy).not.toHaveBeenCalled(); - }); - - it("does not write to disk state if called with memory only", async () => { - await cryptoService.clearProviderKeys(true); - - expect(stateProvider.singleUser.mock.get).not.toHaveBeenCalled(); - }); - - it("clears disk state when called with diskOnly", async () => { - await cryptoService.clearProviderKeys(false); - - expect(stateProvider.singleUser.mock.get).toHaveBeenCalledWith( - mockUserId, - USER_ENCRYPTED_PROVIDER_KEYS, - ); - expect( - stateProvider.singleUser.getFake(mockUserId, USER_ENCRYPTED_PROVIDER_KEYS).nextMock, - ).toHaveBeenCalledWith(null); - }); - - it("clears another user's disk state when called with diskOnly and that user", async () => { - await cryptoService.clearProviderKeys(false, "someOtherUser" as UserId); - - expect(stateProvider.singleUser.mock.get).toHaveBeenCalledWith( - "someOtherUser" as UserId, - USER_ENCRYPTED_PROVIDER_KEYS, - ); - expect( - stateProvider.singleUser.getFake("someOtherUser" as UserId, USER_ENCRYPTED_PROVIDER_KEYS) - .nextMock, - ).toHaveBeenCalledWith(null); - }); - - it("does not clear active user disk state when called with diskOnly and a different specified user", async () => { - await cryptoService.clearProviderKeys(false, "someOtherUser" as UserId); - - expect(stateProvider.singleUser.mock.get).not.toHaveBeenCalledWith( - mockUserId, - USER_ENCRYPTED_PROVIDER_KEYS, - ); - }); - }); - - describe("clearKeyPair", () => { - let forceMemoryPrivateKeySpy: jest.Mock; - let forceMemoryPublicKeySpy: jest.Mock; - beforeEach(() => { - forceMemoryPrivateKeySpy = cryptoService["activeUserPrivateKeyState"].forceValue = jest.fn(); - forceMemoryPublicKeySpy = cryptoService["activeUserPublicKeyState"].forceValue = jest.fn(); - }); - it("clears in memory org keys when called with memoryOnly", async () => { - await cryptoService.clearKeyPair(true); - - expect(forceMemoryPrivateKeySpy).toHaveBeenCalledWith(null); - expect(forceMemoryPublicKeySpy).toHaveBeenCalledWith(null); - }); - - it("does not clear memory when called with the non active user and memory only", async () => { - await cryptoService.clearKeyPair(true, "someOtherUser" as UserId); - - expect(forceMemoryPrivateKeySpy).not.toHaveBeenCalled(); - expect(forceMemoryPublicKeySpy).not.toHaveBeenCalled(); - }); - - it("does not write to disk state if called with memory only", async () => { - await cryptoService.clearKeyPair(true); - - expect(stateProvider.singleUser.mock.get).not.toHaveBeenCalled(); - }); - - it("clears disk state when called with diskOnly", async () => { - await cryptoService.clearKeyPair(false); - - expect(stateProvider.singleUser.mock.get).toHaveBeenCalledWith( - mockUserId, - USER_ENCRYPTED_PRIVATE_KEY, - ); - expect( - stateProvider.singleUser.getFake(mockUserId, USER_ENCRYPTED_PRIVATE_KEY).nextMock, - ).toHaveBeenCalledWith(null); - }); - - it("clears another user's disk state when called with diskOnly and that user", async () => { - await cryptoService.clearKeyPair(false, "someOtherUser" as UserId); - - expect(stateProvider.singleUser.mock.get).toHaveBeenCalledWith( - "someOtherUser" as UserId, - USER_ENCRYPTED_PRIVATE_KEY, - ); - expect( - stateProvider.singleUser.getFake("someOtherUser" as UserId, USER_ENCRYPTED_PRIVATE_KEY) - .nextMock, - ).toHaveBeenCalledWith(null); - }); - - it("does not clear active user disk state when called with diskOnly and a different specified user", async () => { - await cryptoService.clearKeyPair(false, "someOtherUser" as UserId); - - expect(stateProvider.singleUser.mock.get).not.toHaveBeenCalledWith( - mockUserId, - USER_ENCRYPTED_PRIVATE_KEY, - ); - }); - }); - - describe("clearUserKey", () => { - it("clears the user key for the active user when no userId is specified", async () => { - await cryptoService.clearUserKey(false); - expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(USER_KEY, null, undefined); - }); - - it("clears the user key for the specified user when a userId is specified", async () => { - await cryptoService.clearUserKey(false, "someOtherUser" as UserId); - expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(USER_KEY, null, "someOtherUser"); - }); - - it("sets the maximum account status of the active user id to locked when user id is not specified", async () => { - await cryptoService.clearUserKey(false); - expect(accountService.mock.setMaxAccountStatus).toHaveBeenCalledWith( - mockUserId, - AuthenticationStatus.Locked, - ); - }); - - it("sets the maximum account status of the specified user id to locked when user id is specified", async () => { - await cryptoService.clearUserKey(false, "someOtherUser" as UserId); - expect(accountService.mock.setMaxAccountStatus).toHaveBeenCalledWith( - "someOtherUser" as UserId, - AuthenticationStatus.Locked, - ); - }); - - it("clears all stored user keys when clearAll is true", async () => { - const clearAllSpy = (cryptoService["clearAllStoredUserKeys"] = jest.fn()); - await cryptoService.clearUserKey(true); - expect(clearAllSpy).toHaveBeenCalledWith(mockUserId); + const encryptedOrgKeyState = stateProvider.singleUser.getFake(userId, key); + expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledTimes(1); + expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledWith(null); + }); }); }); }); diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index dd3c497470..3cd443c073 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -6,7 +6,7 @@ import { ProfileOrganizationResponse } from "../../admin-console/models/response import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response"; import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response"; import { AccountService } from "../../auth/abstractions/account.service"; -import { AuthenticationStatus } from "../../auth/enums/authentication-status"; +import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { Utils } from "../../platform/misc/utils"; import { CsprngArray } from "../../types/csprng"; @@ -82,6 +82,7 @@ export class CryptoService implements CryptoServiceAbstraction { readonly everHadUserKey$: Observable; constructor( + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected keyGenerationService: KeyGenerationService, protected cryptoFunctionService: CryptoFunctionService, protected encryptService: EncryptService, @@ -144,14 +145,12 @@ export class CryptoService implements CryptoServiceAbstraction { async setUserKey(key: UserKey, userId?: UserId): Promise { if (key == null) { - throw new Error("No key provided. Use ClearUserKey to clear the key"); + throw new Error("No key provided. Lock the user to clear the key"); } // Set userId to ensure we have one for the account status update [userId, key] = await this.stateProvider.setUserState(USER_KEY, key, userId); await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, true, userId); - await this.accountService.setAccountStatus(userId, AuthenticationStatus.Unlocked); - await this.storeAdditionalKeys(key, userId); } @@ -165,28 +164,21 @@ export class CryptoService implements CryptoServiceAbstraction { } async getUserKey(userId?: UserId): Promise { - let userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId)); - if (userKey) { - return userKey; - } - - // If the user has set their vault timeout to 'Never', we can load the user key from storage - if (await this.hasUserKeyStored(KeySuffixOptions.Auto, userId)) { - userKey = await this.getKeyFromStorage(KeySuffixOptions.Auto, userId); - if (userKey) { - await this.setUserKey(userKey, userId); - return userKey; - } - } + const userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId)); + return userKey; } async isLegacyUser(masterKey?: MasterKey, userId?: UserId): Promise { - return await this.validateUserKey( - (masterKey ?? (await this.getMasterKey(userId))) as unknown as UserKey, - ); + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + masterKey ??= await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + + return await this.validateUserKey(masterKey as unknown as UserKey); } + // TODO: legacy support for user key is no longer needed since we require users to migrate on login async getUserKeyWithLegacySupport(userId?: UserId): Promise { + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + const userKey = await this.getUserKey(userId); if (userKey) { return userKey; @@ -194,7 +186,8 @@ export class CryptoService implements CryptoServiceAbstraction { // Legacy support: encryption used to be done with the master key (derived from master password). // Users who have not migrated will have a null user key and must use the master key instead. - return (await this.getMasterKey(userId)) as unknown as UserKey; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + return masterKey as unknown as UserKey; } async getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: UserId): Promise { @@ -213,10 +206,7 @@ export class CryptoService implements CryptoServiceAbstraction { if (userId == null) { return false; } - return ( - (await this.hasUserKeyInMemory(userId)) || - (await this.hasUserKeyStored(KeySuffixOptions.Auto, userId)) - ); + return await this.hasUserKeyInMemory(userId); } async hasUserKeyInMemory(userId?: UserId): Promise { @@ -233,7 +223,10 @@ export class CryptoService implements CryptoServiceAbstraction { } async makeUserKey(masterKey: MasterKey): Promise<[UserKey, EncString]> { - masterKey ||= await this.getMasterKey(); + if (!masterKey) { + const userId = await firstValueFrom(this.stateProvider.activeUserId$); + masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + } if (masterKey == null) { throw new Error("No Master Key found."); } @@ -242,13 +235,18 @@ export class CryptoService implements CryptoServiceAbstraction { return this.buildProtectedSymmetricKey(masterKey, newUserKey.key); } - async clearUserKey(clearStoredKeys = true, userId?: UserId): Promise { - // Set userId to ensure we have one for the account status update - [userId] = await this.stateProvider.setUserState(USER_KEY, null, userId); - await this.accountService.setMaxAccountStatus(userId, AuthenticationStatus.Locked); - if (clearStoredKeys) { - await this.clearAllStoredUserKeys(userId); + /** + * Clears the user key. Clears all stored versions of the user keys as well, such as the biometrics key + * @param userId The desired user + */ + private async clearUserKey(userId: UserId): Promise { + if (userId == null) { + // nothing to do + return; } + // Set userId to ensure we have one for the account status update + await this.stateProvider.setUserState(USER_KEY, null, userId); + await this.clearAllStoredUserKeys(userId); } async clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise { @@ -271,28 +269,17 @@ export class CryptoService implements CryptoServiceAbstraction { } async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId?: UserId): Promise { - await this.stateService.setMasterKeyEncryptedUserKey(userKeyMasterKey, { userId: userId }); - } - - async setMasterKey(key: MasterKey, userId?: UserId): Promise { - await this.stateService.setMasterKey(key, { userId: userId }); - } - - async getMasterKey(userId?: UserId): Promise { - let masterKey = await this.stateService.getMasterKey({ userId: userId }); - if (!masterKey) { - masterKey = (await this.stateService.getCryptoMasterKey({ userId: userId })) as MasterKey; - // if master key was null/undefined and getCryptoMasterKey also returned null/undefined, - // don't set master key as it is unnecessary - if (masterKey) { - await this.setMasterKey(masterKey, userId); - } - } - return masterKey; + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + await this.masterPasswordService.setMasterKeyEncryptedUserKey( + new EncString(userKeyMasterKey), + userId, + ); } + // TODO: Move to MasterPasswordService async getOrDeriveMasterKey(password: string, userId?: UserId) { - let masterKey = await this.getMasterKey(userId); + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); return (masterKey ||= await this.makeMasterKey( password, await this.stateService.getEmail({ userId: userId }), @@ -306,6 +293,7 @@ export class CryptoService implements CryptoServiceAbstraction { * * @remarks * Does not validate the kdf config to ensure it satisfies the minimum requirements for the given kdf type. + * TODO: Move to MasterPasswordService */ async makeMasterKey( password: string, @@ -321,10 +309,6 @@ export class CryptoService implements CryptoServiceAbstraction { )) as MasterKey; } - async clearMasterKey(userId?: UserId): Promise { - await this.stateService.setMasterKey(null, { userId: userId }); - } - async encryptUserKeyWithMasterKey( masterKey: MasterKey, userKey?: UserKey, @@ -333,32 +317,28 @@ export class CryptoService implements CryptoServiceAbstraction { return await this.buildProtectedSymmetricKey(masterKey, userKey.key); } + // TODO: move to master password service async decryptUserKeyWithMasterKey( masterKey: MasterKey, userKey?: EncString, userId?: UserId, ): Promise { - masterKey ||= await this.getMasterKey(userId); + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + userKey ??= await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId); + masterKey ??= await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (masterKey == null) { throw new Error("No master key found."); } - if (!userKey) { - let masterKeyEncryptedUserKey = await this.stateService.getMasterKeyEncryptedUserKey({ + // Try one more way to get the user key if it still wasn't found. + if (userKey == null) { + const deprecatedKey = await this.stateService.getEncryptedCryptoSymmetricKey({ userId: userId, }); - - // Try one more way to get the user key if it still wasn't found. - if (masterKeyEncryptedUserKey == null) { - masterKeyEncryptedUserKey = await this.stateService.getEncryptedCryptoSymmetricKey({ - userId: userId, - }); - } - - if (masterKeyEncryptedUserKey == null) { + if (deprecatedKey == null) { throw new Error("No encrypted user key found."); } - userKey = new EncString(masterKeyEncryptedUserKey); + userKey = new EncString(deprecatedKey); } let decUserKey: Uint8Array; @@ -377,12 +357,16 @@ export class CryptoService implements CryptoServiceAbstraction { return new SymmetricCryptoKey(decUserKey) as UserKey; } + // TODO: move to MasterPasswordService async hashMasterKey( password: string, key: MasterKey, hashPurpose?: HashPurpose, ): Promise { - key ||= await this.getMasterKey(); + if (!key) { + const userId = await firstValueFrom(this.stateProvider.activeUserId$); + key = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + } if (password == null || key == null) { throw new Error("Invalid parameters."); @@ -393,20 +377,12 @@ export class CryptoService implements CryptoServiceAbstraction { return Utils.fromBufferToB64(hash); } - async setMasterKeyHash(keyHash: string): Promise { - await this.stateService.setKeyHash(keyHash); - } - - async getMasterKeyHash(): Promise { - return await this.stateService.getKeyHash(); - } - - async clearMasterKeyHash(userId?: UserId): Promise { - return await this.stateService.setKeyHash(null, { userId: userId }); - } - + // TODO: move to MasterPasswordService async compareAndUpdateKeyHash(masterPassword: string, masterKey: MasterKey): Promise { - const storedPasswordHash = await this.getMasterKeyHash(); + const userId = await firstValueFrom(this.stateProvider.activeUserId$); + const storedPasswordHash = await firstValueFrom( + this.masterPasswordService.masterKeyHash$(userId), + ); if (masterPassword != null && storedPasswordHash != null) { const localKeyHash = await this.hashMasterKey( masterPassword, @@ -424,7 +400,7 @@ export class CryptoService implements CryptoServiceAbstraction { HashPurpose.ServerAuthorization, ); if (serverKeyHash != null && storedPasswordHash === serverKeyHash) { - await this.setMasterKeyHash(localKeyHash); + await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId); return true; } } @@ -480,25 +456,12 @@ export class CryptoService implements CryptoServiceAbstraction { return this.buildProtectedSymmetricKey(key, newSymKey.key); } - async clearOrgKeys(memoryOnly?: boolean, userId?: UserId): Promise { - const activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - const userIdIsActive = userId == null || userId === activeUserId; - - if (!memoryOnly) { - if (userId == null && activeUserId == null) { - // nothing to do - return; - } - await this.stateProvider - .getUser(userId ?? activeUserId, USER_ENCRYPTED_ORGANIZATION_KEYS) - .update(() => null); + private async clearOrgKeys(userId: UserId): Promise { + if (userId == null) { + // nothing to do return; } - - // org keys are only cached for active users - if (userIdIsActive) { - await this.activeUserOrgKeysState.forceValue({}); - } + await this.stateProvider.setUserState(USER_ENCRYPTED_ORGANIZATION_KEYS, null, userId); } async setProviderKeys(providers: ProfileProviderResponse[]): Promise { @@ -526,25 +489,12 @@ export class CryptoService implements CryptoServiceAbstraction { return await firstValueFrom(this.activeUserProviderKeys$); } - async clearProviderKeys(memoryOnly?: boolean, userId?: UserId): Promise { - const activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - const userIdIsActive = userId == null || userId === activeUserId; - - if (!memoryOnly) { - if (userId == null && activeUserId == null) { - // nothing to do - return; - } - await this.stateProvider - .getUser(userId ?? activeUserId, USER_ENCRYPTED_PROVIDER_KEYS) - .update(() => null); + private async clearProviderKeys(userId: UserId): Promise { + if (userId == null) { + // nothing to do return; } - - // provider keys are only cached for active users - if (userIdIsActive) { - await this.activeUserProviderKeysState.forceValue({}); - } + await this.stateProvider.setUserState(USER_ENCRYPTED_PROVIDER_KEYS, null, userId); } async getPublicKey(): Promise { @@ -597,26 +547,17 @@ export class CryptoService implements CryptoServiceAbstraction { return [publicB64, privateEnc]; } - async clearKeyPair(memoryOnly?: boolean, userId?: UserId): Promise { - const activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - const userIdIsActive = userId == null || userId === activeUserId; - - if (!memoryOnly) { - if (userId == null && activeUserId == null) { - // nothing to do - return; - } - await this.stateProvider - .getUser(userId ?? activeUserId, USER_ENCRYPTED_PRIVATE_KEY) - .update(() => null); + /** + * Clears the user's key pair + * @param userId The desired user + */ + private async clearKeyPair(userId: UserId): Promise { + if (userId == null) { + // nothing to do return; } - // decrypted key pair is only cached for active users - if (userIdIsActive) { - await this.activeUserPrivateKeyState.forceValue(null); - await this.activeUserPublicKeyState.forceValue(null); - } + await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId); } async makePinKey(pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig): Promise { @@ -681,11 +622,17 @@ export class CryptoService implements CryptoServiceAbstraction { } async clearKeys(userId?: UserId): Promise { - await this.clearUserKey(true, userId); - await this.clearMasterKeyHash(userId); - await this.clearOrgKeys(false, userId); - await this.clearProviderKeys(false, userId); - await this.clearKeyPair(false, userId); + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + + if (userId == null) { + throw new Error("Cannot clear keys, no user Id resolved."); + } + + await this.masterPasswordService.clearMasterKeyHash(userId); + await this.clearUserKey(userId); + await this.clearOrgKeys(userId); + await this.clearProviderKeys(userId); + await this.clearKeyPair(userId); await this.clearPinKeys(userId); await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, null, userId); } @@ -1037,7 +984,8 @@ export class CryptoService implements CryptoServiceAbstraction { if (await this.isLegacyUser(masterKey, userId)) { // Legacy users don't have a user key, so no need to migrate. // Instead, set the master key for additional isLegacyUser checks that will log the user out. - await this.setMasterKey(masterKey, userId); + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + await this.masterPasswordService.setMasterKey(masterKey, userId); return; } const encryptedUserKey = await this.stateService.getEncryptedCryptoSymmetricKey({ diff --git a/libs/common/src/platform/services/default-broadcaster.service.ts b/libs/common/src/platform/services/default-broadcaster.service.ts new file mode 100644 index 0000000000..a16745c643 --- /dev/null +++ b/libs/common/src/platform/services/default-broadcaster.service.ts @@ -0,0 +1,36 @@ +import { Subscription } from "rxjs"; + +import { BroadcasterService, MessageBase } from "../abstractions/broadcaster.service"; +import { MessageListener, MessageSender } from "../messaging"; + +/** + * Temporary implementation that just delegates to the message sender and message listener + * and manages their subscriptions. + */ +export class DefaultBroadcasterService implements BroadcasterService { + subscriptions = new Map(); + + constructor( + private readonly messageSender: MessageSender, + private readonly messageListener: MessageListener, + ) {} + + send(message: MessageBase, id?: string) { + this.messageSender.send(message?.command, message); + } + + subscribe(id: string, messageCallback: (message: MessageBase) => void) { + this.subscriptions.set( + id, + this.messageListener.allMessages$.subscribe((message) => { + messageCallback(message); + }), + ); + } + + unsubscribe(id: string) { + const subscription = this.subscriptions.get(id); + subscription?.unsubscribe(); + this.subscriptions.delete(id); + } +} diff --git a/libs/common/src/platform/services/default-environment.service.spec.ts b/libs/common/src/platform/services/default-environment.service.spec.ts index 66bc1acfda..dd504dc302 100644 --- a/libs/common/src/platform/services/default-environment.service.spec.ts +++ b/libs/common/src/platform/services/default-environment.service.spec.ts @@ -2,14 +2,14 @@ import { firstValueFrom } from "rxjs"; import { FakeStateProvider, awaitAsync } from "../../../spec"; import { FakeAccountService } from "../../../spec/fake-account-service"; -import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { UserId } from "../../types/guid"; import { CloudRegion, Region } from "../abstractions/environment.service"; import { - ENVIRONMENT_KEY, + GLOBAL_ENVIRONMENT_KEY, DefaultEnvironmentService, EnvironmentUrls, + USER_ENVIRONMENT_KEY, } from "./default-environment.service"; // There are a few main states EnvironmentService could be in when first used @@ -31,12 +31,10 @@ describe("EnvironmentService", () => { [testUser]: { name: "name", email: "email", - status: AuthenticationStatus.Locked, }, [alternateTestUser]: { name: "name", email: "email", - status: AuthenticationStatus.Locked, }, }); stateProvider = new FakeStateProvider(accountService); @@ -49,13 +47,12 @@ describe("EnvironmentService", () => { id: userId, email: "test@example.com", name: `Test Name ${userId}`, - status: AuthenticationStatus.Unlocked, }); await awaitAsync(); }; const setGlobalData = (region: Region, environmentUrls: EnvironmentUrls) => { - stateProvider.global.getFake(ENVIRONMENT_KEY).stateSubject.next({ + stateProvider.global.getFake(GLOBAL_ENVIRONMENT_KEY).stateSubject.next({ region: region, urls: environmentUrls, }); @@ -66,7 +63,7 @@ describe("EnvironmentService", () => { environmentUrls: EnvironmentUrls, userId: UserId = testUser, ) => { - stateProvider.singleUser.getFake(userId, ENVIRONMENT_KEY).nextState({ + stateProvider.singleUser.getFake(userId, USER_ENVIRONMENT_KEY).nextState({ region: region, urls: environmentUrls, }); diff --git a/libs/common/src/platform/services/default-environment.service.ts b/libs/common/src/platform/services/default-environment.service.ts index d074ff43f8..59956ede7a 100644 --- a/libs/common/src/platform/services/default-environment.service.ts +++ b/libs/common/src/platform/services/default-environment.service.ts @@ -18,6 +18,7 @@ import { GlobalState, KeyDefinition, StateProvider, + UserKeyDefinition, } from "../state"; export class EnvironmentUrls { @@ -40,7 +41,7 @@ class EnvironmentState { } } -export const ENVIRONMENT_KEY = new KeyDefinition( +export const GLOBAL_ENVIRONMENT_KEY = new KeyDefinition( ENVIRONMENT_DISK, "environment", { @@ -48,9 +49,31 @@ export const ENVIRONMENT_KEY = new KeyDefinition( }, ); -export const CLOUD_REGION_KEY = new KeyDefinition(ENVIRONMENT_MEMORY, "cloudRegion", { - deserializer: (b) => b, -}); +export const USER_ENVIRONMENT_KEY = new UserKeyDefinition( + ENVIRONMENT_DISK, + "environment", + { + deserializer: EnvironmentState.fromJSON, + clearOn: ["logout"], + }, +); + +export const GLOBAL_CLOUD_REGION_KEY = new KeyDefinition( + ENVIRONMENT_MEMORY, + "cloudRegion", + { + deserializer: (b) => b, + }, +); + +export const USER_CLOUD_REGION_KEY = new UserKeyDefinition( + ENVIRONMENT_MEMORY, + "cloudRegion", + { + deserializer: (b) => b, + clearOn: ["logout"], + }, +); /** * The production regions available for selection. @@ -114,8 +137,8 @@ export class DefaultEnvironmentService implements EnvironmentService { private stateProvider: StateProvider, private accountService: AccountService, ) { - this.globalState = this.stateProvider.getGlobal(ENVIRONMENT_KEY); - this.globalCloudRegionState = this.stateProvider.getGlobal(CLOUD_REGION_KEY); + this.globalState = this.stateProvider.getGlobal(GLOBAL_ENVIRONMENT_KEY); + this.globalCloudRegionState = this.stateProvider.getGlobal(GLOBAL_CLOUD_REGION_KEY); const account$ = this.activeAccountId$.pipe( // Use == here to not trigger on undefined -> null transition @@ -125,8 +148,8 @@ export class DefaultEnvironmentService implements EnvironmentService { this.environment$ = account$.pipe( switchMap((userId) => { const t = userId - ? this.stateProvider.getUser(userId, ENVIRONMENT_KEY).state$ - : this.stateProvider.getGlobal(ENVIRONMENT_KEY).state$; + ? this.stateProvider.getUser(userId, USER_ENVIRONMENT_KEY).state$ + : this.stateProvider.getGlobal(GLOBAL_ENVIRONMENT_KEY).state$; return t; }), map((state) => { @@ -136,8 +159,8 @@ export class DefaultEnvironmentService implements EnvironmentService { this.cloudWebVaultUrl$ = account$.pipe( switchMap((userId) => { const t = userId - ? this.stateProvider.getUser(userId, CLOUD_REGION_KEY).state$ - : this.stateProvider.getGlobal(CLOUD_REGION_KEY).state$; + ? this.stateProvider.getUser(userId, USER_CLOUD_REGION_KEY).state$ + : this.stateProvider.getGlobal(GLOBAL_CLOUD_REGION_KEY).state$; return t; }), map((region) => { @@ -242,7 +265,7 @@ export class DefaultEnvironmentService implements EnvironmentService { if (userId == null) { await this.globalCloudRegionState.update(() => region); } else { - await this.stateProvider.getUser(userId, CLOUD_REGION_KEY).update(() => region); + await this.stateProvider.getUser(userId, USER_CLOUD_REGION_KEY).update(() => region); } } @@ -261,13 +284,13 @@ export class DefaultEnvironmentService implements EnvironmentService { return activeUserId == null ? await firstValueFrom(this.globalState.state$) : await firstValueFrom( - this.stateProvider.getUser(userId ?? activeUserId, ENVIRONMENT_KEY).state$, + this.stateProvider.getUser(userId ?? activeUserId, USER_ENVIRONMENT_KEY).state$, ); } async seedUserEnvironment(userId: UserId) { const global = await firstValueFrom(this.globalState.state$); - await this.stateProvider.getUser(userId, ENVIRONMENT_KEY).update(() => global); + await this.stateProvider.getUser(userId, USER_ENVIRONMENT_KEY).update(() => global); } } diff --git a/libs/common/src/platform/services/key-state/org-keys.state.ts b/libs/common/src/platform/services/key-state/org-keys.state.ts index b39cc9a82a..f67e64b653 100644 --- a/libs/common/src/platform/services/key-state/org-keys.state.ts +++ b/libs/common/src/platform/services/key-state/org-keys.state.ts @@ -4,13 +4,14 @@ import { OrganizationId } from "../../../types/guid"; import { OrgKey } from "../../../types/key"; import { CryptoService } from "../../abstractions/crypto.service"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; -import { KeyDefinition, CRYPTO_DISK, DeriveDefinition } from "../../state"; +import { CRYPTO_DISK, DeriveDefinition, UserKeyDefinition } from "../../state"; -export const USER_ENCRYPTED_ORGANIZATION_KEYS = KeyDefinition.record< +export const USER_ENCRYPTED_ORGANIZATION_KEYS = UserKeyDefinition.record< EncryptedOrganizationKeyData, OrganizationId >(CRYPTO_DISK, "organizationKeys", { deserializer: (obj) => obj, + clearOn: ["logout"], }); export const USER_ORGANIZATION_KEYS = DeriveDefinition.from< diff --git a/libs/common/src/platform/services/key-state/provider-keys.state.ts b/libs/common/src/platform/services/key-state/provider-keys.state.ts index c89df34c80..776fdc77d8 100644 --- a/libs/common/src/platform/services/key-state/provider-keys.state.ts +++ b/libs/common/src/platform/services/key-state/provider-keys.state.ts @@ -3,14 +3,15 @@ import { ProviderKey } from "../../../types/key"; import { EncryptService } from "../../abstractions/encrypt.service"; import { EncString, EncryptedString } from "../../models/domain/enc-string"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; -import { KeyDefinition, CRYPTO_DISK, DeriveDefinition } from "../../state"; +import { CRYPTO_DISK, DeriveDefinition, UserKeyDefinition } from "../../state"; import { CryptoService } from "../crypto.service"; -export const USER_ENCRYPTED_PROVIDER_KEYS = KeyDefinition.record( +export const USER_ENCRYPTED_PROVIDER_KEYS = UserKeyDefinition.record( CRYPTO_DISK, "providerKeys", { deserializer: (obj) => obj, + clearOn: ["logout"], }, ); diff --git a/libs/common/src/platform/services/key-state/user-key.state.ts b/libs/common/src/platform/services/key-state/user-key.state.ts index d0f54c9add..609525b0ac 100644 --- a/libs/common/src/platform/services/key-state/user-key.state.ts +++ b/libs/common/src/platform/services/key-state/user-key.state.ts @@ -3,18 +3,24 @@ import { CryptoFunctionService } from "../../abstractions/crypto-function.servic import { EncryptService } from "../../abstractions/encrypt.service"; import { EncString, EncryptedString } from "../../models/domain/enc-string"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; -import { KeyDefinition, CRYPTO_DISK, DeriveDefinition, CRYPTO_MEMORY } from "../../state"; +import { CRYPTO_DISK, DeriveDefinition, CRYPTO_MEMORY, UserKeyDefinition } from "../../state"; import { CryptoService } from "../crypto.service"; -export const USER_EVER_HAD_USER_KEY = new KeyDefinition(CRYPTO_DISK, "everHadUserKey", { - deserializer: (obj) => obj, -}); +export const USER_EVER_HAD_USER_KEY = new UserKeyDefinition( + CRYPTO_DISK, + "everHadUserKey", + { + deserializer: (obj) => obj, + clearOn: ["logout"], + }, +); -export const USER_ENCRYPTED_PRIVATE_KEY = new KeyDefinition( +export const USER_ENCRYPTED_PRIVATE_KEY = new UserKeyDefinition( CRYPTO_DISK, "privateKey", { deserializer: (obj) => obj, + clearOn: ["logout"], }, ); @@ -58,6 +64,7 @@ export const USER_PUBLIC_KEY = DeriveDefinition.from< return (await cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey; }, }); -export const USER_KEY = new KeyDefinition(CRYPTO_MEMORY, "userKey", { +export const USER_KEY = new UserKeyDefinition(CRYPTO_MEMORY, "userKey", { deserializer: (obj) => SymmetricCryptoKey.fromJSON(obj) as UserKey, + clearOn: ["logout", "lock"], }); diff --git a/libs/common/src/platform/services/noop-messaging.service.ts b/libs/common/src/platform/services/noop-messaging.service.ts deleted file mode 100644 index d1a60bc5bc..0000000000 --- a/libs/common/src/platform/services/noop-messaging.service.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { MessagingService } from "../abstractions/messaging.service"; - -export class NoopMessagingService implements MessagingService { - send(subscriber: string, arg: any = {}) { - // Do nothing... - } -} diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index a35659a7ac..d0a55d7a47 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -1,22 +1,14 @@ -import { BehaviorSubject, Observable, map } from "rxjs"; +import { BehaviorSubject } from "rxjs"; import { Jsonify, JsonValue } from "type-fest"; import { AccountService } from "../../auth/abstractions/account.service"; import { TokenService } from "../../auth/abstractions/token.service"; -import { AuthenticationStatus } from "../../auth/enums/authentication-status"; -import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable"; -import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { BiometricKey } from "../../auth/types/biometric-key"; import { GeneratorOptions } from "../../tools/generator/generator-options"; import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; import { UsernameGeneratorOptions } from "../../tools/generator/username"; import { UserId } from "../../types/guid"; -import { MasterKey } from "../../types/key"; -import { CipherData } from "../../vault/models/data/cipher.data"; -import { LocalData } from "../../vault/models/data/local.data"; -import { CipherView } from "../../vault/models/view/cipher.view"; -import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info"; import { EnvironmentService } from "../abstractions/environment.service"; import { LogService } from "../abstractions/log.service"; import { @@ -35,7 +27,6 @@ import { EncString } from "../models/domain/enc-string"; import { GlobalState } from "../models/domain/global-state"; import { State } from "../models/domain/state"; import { StorageOptions } from "../models/domain/storage-options"; -import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; import { MigrationRunner } from "./migration-runner"; @@ -71,8 +62,6 @@ export class StateService< protected activeAccountSubject = new BehaviorSubject(null); activeAccount$ = this.activeAccountSubject.asObservable(); - activeAccountUnlocked$: Observable; - private hasBeenInited = false; protected isRecoveredSession = false; @@ -92,13 +81,7 @@ export class StateService< protected tokenService: TokenService, private migrationRunner: MigrationRunner, protected useAccountCache: boolean = true, - ) { - this.activeAccountUnlocked$ = this.accountService.activeAccount$.pipe( - map((a) => { - return a?.status === AuthenticationStatus.Unlocked; - }), - ); - } + ) {} async init(initOptions: InitOptions = {}): Promise { // Deconstruct and apply defaults @@ -154,7 +137,6 @@ export class StateService< await this.accountService.addAccount(state.activeUserId as UserId, { name: activeDiskAccount.profile.name, email: activeDiskAccount.profile.email, - status: AuthenticationStatus.LoggedOut, }); } await this.accountService.switchAccount(state.activeUserId as UserId); @@ -180,16 +162,7 @@ export class StateService< // TODO: Temporary update to avoid routing all account status changes through account service for now. // The determination of state should be handled by the various services that control those values. - const token = await this.tokenService.getAccessToken(userId as UserId); - const autoKey = await this.getUserKeyAutoUnlock({ userId: userId }); - const accountStatus = - token == null - ? AuthenticationStatus.LoggedOut - : autoKey == null - ? AuthenticationStatus.Locked - : AuthenticationStatus.Unlocked; await this.accountService.addAccount(userId as UserId, { - status: accountStatus, name: diskAccount.profile.name, email: diskAccount.profile.email, }); @@ -209,7 +182,6 @@ export class StateService< await this.setLastActive(new Date().getTime(), { userId: account.profile.userId }); // TODO: Temporary update to avoid routing all account status changes through account service for now. await this.accountService.addAccount(account.profile.userId as UserId, { - status: AuthenticationStatus.Locked, name: account.profile.name, email: account.profile.email, }); @@ -245,93 +217,6 @@ export class StateService< return currentUser as UserId; } - async getAddEditCipherInfo(options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - // ensure prototype on cipher - const raw = account?.data?.addEditCipherInfo; - return raw == null - ? null - : { - cipher: - raw?.cipher.toJSON != null - ? raw.cipher - : CipherView.fromJSON(raw?.cipher as Jsonify), - collectionIds: raw?.collectionIds, - }; - } - - async setAddEditCipherInfo(value: AddEditCipherInfo, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.data.addEditCipherInfo = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - - /** - * @deprecated Do not save the Master Key. Use the User Symmetric Key instead - */ - async getCryptoMasterKey(options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - return account?.keys?.cryptoMasterKey; - } - - /** - * User's master key derived from MP, saved only if we decrypted with MP - */ - async getMasterKey(options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - return account?.keys?.masterKey; - } - - /** - * User's master key derived from MP, saved only if we decrypted with MP - */ - async setMasterKey(value: MasterKey, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.keys.masterKey = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - - /** - * The master key encrypted User symmetric key, saved on every auth - * so we can unlock with MP offline - */ - async getMasterKeyEncryptedUserKey(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.keys.masterKeyEncryptedUserKey; - } - - /** - * The master key encrypted User symmetric key, saved on every auth - * so we can unlock with MP offline - */ - async setMasterKeyEncryptedUserKey(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.keys.masterKeyEncryptedUserKey = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - /** * user key when using the "never" option of vault timeout */ @@ -548,24 +433,6 @@ export class StateService< await this.saveSecureStorageKey(partialKeys.biometricKey, value, options); } - @withPrototypeForArrayMembers(CipherView, CipherView.fromJSON) - async getDecryptedCiphers(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.data?.ciphers?.decrypted; - } - - async setDecryptedCiphers(value: CipherView[], options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.data.ciphers.decrypted = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - @withPrototypeForArrayMembers(GeneratedPasswordHistory) async getDecryptedPasswordGenerationHistory( options?: StorageOptions, @@ -630,37 +497,6 @@ export class StateService< : await this.secureStorageService.save(DDG_SHARED_KEY, value, options); } - async getAdminAuthRequest(options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()); - - if (options?.userId == null) { - return null; - } - - const account = await this.getAccount(options); - - return account?.adminAuthRequest - ? AdminAuthRequestStorable.fromJSON(account.adminAuthRequest) - : null; - } - - async setAdminAuthRequest( - adminAuthRequest: AdminAuthRequestStorable, - options?: StorageOptions, - ): Promise { - options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()); - - if (options?.userId == null) { - return; - } - - const account = await this.getAccount(options); - - account.adminAuthRequest = adminAuthRequest?.toJSON(); - - await this.saveAccount(account, options); - } - async getEmail(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) @@ -735,24 +571,17 @@ export class StateService< ); } - @withPrototypeForObjectValues(CipherData) - async getEncryptedCiphers(options?: StorageOptions): Promise<{ [id: string]: CipherData }> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())) - )?.data?.ciphers?.encrypted; - } - - async setEncryptedCiphers( - value: { [id: string]: CipherData }, + async setEnableDuckDuckGoBrowserIntegration( + value: boolean, options?: StorageOptions, ): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskOptions()), ); - account.data.ciphers.encrypted = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), + globals.enableDuckDuckGoBrowserIntegration = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskOptions()), ); } @@ -805,48 +634,6 @@ export class StateService< ); } - async getEverBeenUnlocked(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))) - ?.profile?.everBeenUnlocked ?? false - ); - } - - async setEverBeenUnlocked(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.profile.everBeenUnlocked = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - - async getForceSetPasswordReason(options?: StorageOptions): Promise { - return ( - ( - await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), - ) - )?.profile?.forceSetPasswordReason ?? ForceSetPasswordReason.None - ); - } - - async setForceSetPasswordReason( - value: ForceSetPasswordReason, - options?: StorageOptions, - ): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), - ); - account.profile.forceSetPasswordReason = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), - ); - } - async getIsAuthenticated(options?: StorageOptions): Promise { return ( (await this.tokenService.getAccessToken(options?.userId as UserId)) != null && @@ -897,23 +684,6 @@ export class StateService< ); } - async getKeyHash(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.keyHash; - } - - async setKeyHash(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.keyHash = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getLastActive(options?: StorageOptions): Promise { options = this.reconcileOptions(options, await this.defaultOnDiskOptions()); @@ -960,26 +730,6 @@ export class StateService< ); } - async getLocalData(options?: StorageOptions): Promise<{ [cipherId: string]: LocalData }> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.data?.localData; - } - - async setLocalData( - value: { [cipherId: string]: LocalData }, - options?: StorageOptions, - ): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - account.data.localData = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - async getMinimizeOnCopyToClipboard(options?: StorageOptions): Promise { return ( (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) @@ -1089,23 +839,6 @@ export class StateService< ); } - async getSecurityStamp(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.tokens?.securityStamp; - } - - async setSecurityStamp(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.tokens.securityStamp = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - async getUserId(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) @@ -1155,24 +888,6 @@ export class StateService< ); } - async getApproveLoginRequests(options?: StorageOptions): Promise { - const approveLoginRequests = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.settings?.approveLoginRequests; - return approveLoginRequests; - } - - async setApproveLoginRequests(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - account.settings.approveLoginRequests = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - protected async getGlobals(options: StorageOptions): Promise { let globals: TGlobalState; if (this.useMemory(options.storageLocation)) { @@ -1509,15 +1224,12 @@ export class StateService< return state; }); - // TODO: Invert this logic, we should remove accounts based on logged out emit - await this.accountService.setAccountStatus(userId as UserId, AuthenticationStatus.LoggedOut); } // settings persist even on reset, and are not affected by this method protected resetAccount(account: TAccount) { const persistentAccountInformation = { settings: account.settings, - adminAuthRequest: account.adminAuthRequest, }; return Object.assign(this.createAccount(), persistentAccountInformation); } @@ -1686,50 +1398,3 @@ function withPrototypeForArrayMembers( }; }; } - -function withPrototypeForObjectValues( - valuesConstructor: new (...args: any[]) => T, - valuesConverter: (input: any) => T = (i) => i, -): ( - target: any, - propertyKey: string | symbol, - descriptor: PropertyDescriptor, -) => { value: (...args: any[]) => Promise<{ [key: string]: T }> } { - return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => { - const originalMethod = descriptor.value; - - return { - value: function (...args: any[]) { - const originalResult: Promise<{ [key: string]: T }> = originalMethod.apply(this, args); - - if (!Utils.isPromise(originalResult)) { - throw new Error( - `Error applying prototype to stored value -- result is not a promise for method ${String( - propertyKey, - )}`, - ); - } - - return originalResult.then((result) => { - if (result == null) { - return null; - } else { - for (const [key, val] of Object.entries(result)) { - result[key] = - val == null || val.constructor.name === valuesConstructor.prototype.constructor.name - ? valuesConverter(val) - : valuesConverter( - Object.create( - valuesConstructor.prototype, - Object.getOwnPropertyDescriptors(val), - ), - ); - } - - return result as { [key: string]: T }; - } - }); - }, - }; - }; -} diff --git a/libs/common/src/platform/services/user-key-init.service.spec.ts b/libs/common/src/platform/services/user-key-init.service.spec.ts new file mode 100644 index 0000000000..567320ded6 --- /dev/null +++ b/libs/common/src/platform/services/user-key-init.service.spec.ts @@ -0,0 +1,162 @@ +import { mock } from "jest-mock-extended"; + +import { FakeAccountService, mockAccountServiceWith } from "../../../spec"; +import { CsprngArray } from "../../types/csprng"; +import { UserId } from "../../types/guid"; +import { UserKey } from "../../types/key"; +import { LogService } from "../abstractions/log.service"; +import { KeySuffixOptions } from "../enums"; +import { Utils } from "../misc/utils"; +import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; + +import { CryptoService } from "./crypto.service"; +import { UserKeyInitService } from "./user-key-init.service"; + +describe("UserKeyInitService", () => { + let userKeyInitService: UserKeyInitService; + + const mockUserId = Utils.newGuid() as UserId; + + const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); + + const cryptoService = mock(); + const logService = mock(); + + beforeEach(() => { + userKeyInitService = new UserKeyInitService(accountService, cryptoService, logService); + }); + + describe("listenForActiveUserChangesToSetUserKey()", () => { + it("calls setUserKeyInMemoryIfAutoUserKeySet if there is an active user", () => { + // Arrange + accountService.activeAccountSubject.next({ + id: mockUserId, + name: "name", + email: "email", + }); + + (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet = jest.fn(); + + // Act + + const subscription = userKeyInitService.listenForActiveUserChangesToSetUserKey(); + + // Assert + + expect(subscription).not.toBeFalsy(); + + expect((userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet).toHaveBeenCalledWith( + mockUserId, + ); + }); + + it("calls setUserKeyInMemoryIfAutoUserKeySet if there is an active user and tracks subsequent emissions", () => { + // Arrange + accountService.activeAccountSubject.next({ + id: mockUserId, + name: "name", + email: "email", + }); + + const mockUser2Id = Utils.newGuid() as UserId; + + jest + .spyOn(userKeyInitService as any, "setUserKeyInMemoryIfAutoUserKeySet") + .mockImplementation(() => Promise.resolve()); + + // Act + + const subscription = userKeyInitService.listenForActiveUserChangesToSetUserKey(); + + accountService.activeAccountSubject.next({ + id: mockUser2Id, + name: "name", + email: "email", + }); + + // Assert + + expect(subscription).not.toBeFalsy(); + + expect((userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet).toHaveBeenCalledTimes( + 2, + ); + + expect( + (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet, + ).toHaveBeenNthCalledWith(1, mockUserId); + expect( + (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet, + ).toHaveBeenNthCalledWith(2, mockUser2Id); + + subscription.unsubscribe(); + }); + + it("does not call setUserKeyInMemoryIfAutoUserKeySet if there is not an active user", () => { + // Arrange + accountService.activeAccountSubject.next(null); + + (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet = jest.fn(); + + // Act + + const subscription = userKeyInitService.listenForActiveUserChangesToSetUserKey(); + + // Assert + + expect(subscription).not.toBeFalsy(); + + expect( + (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet, + ).not.toHaveBeenCalledWith(mockUserId); + }); + }); + + describe("setUserKeyInMemoryIfAutoUserKeySet", () => { + it("does nothing if the userId is null", async () => { + // Act + await (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet(null); + + // Assert + expect(cryptoService.getUserKeyFromStorage).not.toHaveBeenCalled(); + expect(cryptoService.setUserKey).not.toHaveBeenCalled(); + }); + + it("does nothing if the autoUserKey is null", async () => { + // Arrange + const userId = mockUserId; + + cryptoService.getUserKeyFromStorage.mockResolvedValue(null); + + // Act + await (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet(userId); + + // Assert + expect(cryptoService.getUserKeyFromStorage).toHaveBeenCalledWith( + KeySuffixOptions.Auto, + userId, + ); + expect(cryptoService.setUserKey).not.toHaveBeenCalled(); + }); + + it("sets the user key in memory if the autoUserKey is not null", async () => { + // Arrange + const userId = mockUserId; + + const mockRandomBytes = new Uint8Array(64) as CsprngArray; + const mockAutoUserKey: UserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; + + cryptoService.getUserKeyFromStorage.mockResolvedValue(mockAutoUserKey); + + // Act + await (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet(userId); + + // Assert + expect(cryptoService.getUserKeyFromStorage).toHaveBeenCalledWith( + KeySuffixOptions.Auto, + userId, + ); + expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockAutoUserKey, userId); + }); + }); +}); diff --git a/libs/common/src/platform/services/user-key-init.service.ts b/libs/common/src/platform/services/user-key-init.service.ts new file mode 100644 index 0000000000..1f6aacce8f --- /dev/null +++ b/libs/common/src/platform/services/user-key-init.service.ts @@ -0,0 +1,57 @@ +import { EMPTY, Subscription, catchError, filter, from, switchMap } from "rxjs"; + +import { AccountService } from "../../auth/abstractions/account.service"; +import { UserId } from "../../types/guid"; +import { CryptoService } from "../abstractions/crypto.service"; +import { LogService } from "../abstractions/log.service"; +import { KeySuffixOptions } from "../enums"; + +// TODO: this is a half measure improvement which allows us to reduce some side effects today (cryptoService.getUserKey setting user key in memory if auto key exists) +// but ideally, in the future, we would be able to put this logic into the cryptoService +// after the vault timeout settings service is transitioned to state provider so that +// the getUserKey logic can simply go to the correct location based on the vault timeout settings +// similar to the TokenService (it would either go to secure storage for the auto user key or memory for the user key) + +export class UserKeyInitService { + constructor( + private accountService: AccountService, + private cryptoService: CryptoService, + private logService: LogService, + ) {} + + // Note: must listen for changes to support account switching + listenForActiveUserChangesToSetUserKey(): Subscription { + return this.accountService.activeAccount$ + .pipe( + filter((activeAccount) => activeAccount != null), + switchMap((activeAccount) => + from(this.setUserKeyInMemoryIfAutoUserKeySet(activeAccount?.id)).pipe( + catchError((err: unknown) => { + this.logService.warning( + `setUserKeyInMemoryIfAutoUserKeySet failed with error: ${err}`, + ); + // Returning EMPTY to protect observable chain from cancellation in case of error + return EMPTY; + }), + ), + ), + ) + .subscribe(); + } + + private async setUserKeyInMemoryIfAutoUserKeySet(userId: UserId) { + if (userId == null) { + return; + } + + const autoUserKey = await this.cryptoService.getUserKeyFromStorage( + KeySuffixOptions.Auto, + userId, + ); + if (autoUserKey == null) { + return; + } + + await this.cryptoService.setUserKey(autoUserKey, userId); + } +} diff --git a/libs/common/src/platform/state/derive-definition.ts b/libs/common/src/platform/state/derive-definition.ts index 6c514f8869..8f62d3a342 100644 --- a/libs/common/src/platform/state/derive-definition.ts +++ b/libs/common/src/platform/state/derive-definition.ts @@ -5,6 +5,7 @@ import { DerivedStateDependencies, StorageKey } from "../../types/state"; import { KeyDefinition } from "./key-definition"; import { StateDefinition } from "./state-definition"; +import { UserKeyDefinition } from "./user-key-definition"; declare const depShapeMarker: unique symbol; /** @@ -129,26 +130,28 @@ export class DeriveDefinition( definition: | KeyDefinition + | UserKeyDefinition | [DeriveDefinition, string], options: DeriveDefinitionOptions, ) { - if (isKeyDefinition(definition)) { - return new DeriveDefinition(definition.stateDefinition, definition.key, options); - } else { + if (isFromDeriveDefinition(definition)) { return new DeriveDefinition(definition[0].stateDefinition, definition[1], options); + } else { + return new DeriveDefinition(definition.stateDefinition, definition.key, options); } } static fromWithUserId( definition: | KeyDefinition + | UserKeyDefinition | [DeriveDefinition, string], options: DeriveDefinitionOptions<[UserId, TKeyDef], TTo, TDeps>, ) { - if (isKeyDefinition(definition)) { - return new DeriveDefinition(definition.stateDefinition, definition.key, options); - } else { + if (isFromDeriveDefinition(definition)) { return new DeriveDefinition(definition[0].stateDefinition, definition[1], options); + } else { + return new DeriveDefinition(definition.stateDefinition, definition.key, options); } } @@ -181,10 +184,11 @@ export class DeriveDefinition + | UserKeyDefinition | [DeriveDefinition, string], -): definition is KeyDefinition { - return Object.prototype.hasOwnProperty.call(definition, "key"); +): definition is [DeriveDefinition, string] { + return Array.isArray(definition); } diff --git a/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts b/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts index 6e01b615d7..51a972a9dc 100644 --- a/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts +++ b/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts @@ -9,7 +9,6 @@ import { Jsonify } from "type-fest"; import { awaitAsync, trackEmissions } from "../../../../spec"; import { FakeStorageService } from "../../../../spec/fake-storage.service"; import { AccountInfo } from "../../../auth/abstractions/account.service"; -import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { UserId } from "../../../types/guid"; import { StorageServiceProvider } from "../../services/storage-service.provider"; import { StateDefinition } from "../state-definition"; @@ -84,7 +83,6 @@ describe("DefaultActiveUserState", () => { id: userId, email: `test${id}@example.com`, name: `Test User ${id}`, - status: AuthenticationStatus.Unlocked, }); await awaitAsync(); }; diff --git a/libs/common/src/platform/state/index.ts b/libs/common/src/platform/state/index.ts index dd14aaf329..367beefb49 100644 --- a/libs/common/src/platform/state/index.ts +++ b/libs/common/src/platform/state/index.ts @@ -8,7 +8,7 @@ export { ActiveUserState, SingleUserState, CombinedState } from "./user-state"; export { ActiveUserStateProvider, SingleUserStateProvider } from "./user-state.provider"; export { KeyDefinition, KeyDefinitionOptions } from "./key-definition"; export { StateUpdateOptions } from "./state-update-options"; -export { UserKeyDefinition } from "./user-key-definition"; +export { UserKeyDefinitionOptions, UserKeyDefinition } from "./user-key-definition"; export { StateEventRunnerService } from "./state-event-runner.service"; export * from "./state-definitions"; diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 979321c1e3..18df252062 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -37,12 +37,17 @@ export const BILLING_DISK = new StateDefinition("billing", "disk"); export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk"); export const ACCOUNT_MEMORY = new StateDefinition("account", "memory"); +export const MASTER_PASSWORD_MEMORY = new StateDefinition("masterPassword", "memory"); +export const MASTER_PASSWORD_DISK = new StateDefinition("masterPassword", "disk"); export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" }); export const ROUTER_DISK = new StateDefinition("router", "disk"); export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", { web: "disk-local", }); export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory"); +export const AUTH_REQUEST_DISK_LOCAL = new StateDefinition("authRequestLocal", "disk", { + web: "disk-local", +}); export const SSO_DISK = new StateDefinition("ssoLogin", "disk"); export const TOKEN_DISK = new StateDefinition("token", "disk"); export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", { @@ -74,6 +79,10 @@ export const NEW_WEB_LAYOUT_BANNER_DISK = new StateDefinition("newWebLayoutBanne web: "disk-local", }); +export const UNASSIGNED_ITEMS_BANNER_DISK = new StateDefinition("unassignedItemsBanner", "disk", { + web: "disk-local", +}); + // Platform export const APPLICATION_ID_DISK = new StateDefinition("applicationId", "disk", { @@ -102,6 +111,7 @@ export const SM_ONBOARDING_DISK = new StateDefinition("smOnboarding", "disk", { export const GENERATOR_DISK = new StateDefinition("generator", "disk"); export const GENERATOR_MEMORY = new StateDefinition("generator", "memory"); +export const BROWSER_SEND_MEMORY = new StateDefinition("sendBrowser", "memory"); export const EVENT_COLLECTION_DISK = new StateDefinition("eventCollection", "disk"); export const SEND_DISK = new StateDefinition("encryptedSend", "disk", { web: "memory", @@ -124,3 +134,9 @@ export const VAULT_SETTINGS_DISK = new StateDefinition("vaultSettings", "disk", web: "disk-local", }); export const VAULT_BROWSER_MEMORY = new StateDefinition("vaultBrowser", "memory"); +export const VAULT_SEARCH_MEMORY = new StateDefinition("vaultSearch", "memory"); +export const CIPHERS_DISK = new StateDefinition("ciphers", "disk", { web: "memory" }); +export const CIPHERS_DISK_LOCAL = new StateDefinition("ciphersLocal", "disk", { + web: "disk-local", +}); +export const CIPHERS_MEMORY = new StateDefinition("ciphersMemory", "memory"); diff --git a/libs/common/src/platform/state/user-key-definition.ts b/libs/common/src/platform/state/user-key-definition.ts index 3405b38837..3eb9369080 100644 --- a/libs/common/src/platform/state/user-key-definition.ts +++ b/libs/common/src/platform/state/user-key-definition.ts @@ -8,7 +8,7 @@ import { StateDefinition } from "./state-definition"; export type ClearEvent = "lock" | "logout"; -type UserKeyDefinitionOptions = KeyDefinitionOptions & { +export type UserKeyDefinitionOptions = KeyDefinitionOptions & { clearOn: ClearEvent[]; }; diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 6306eb1e28..e8135f3d6c 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -7,8 +7,6 @@ import { OrganizationSponsorshipRedeemRequest } from "../admin-console/models/re import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request"; import { ProviderAddOrganizationRequest } from "../admin-console/models/request/provider/provider-add-organization.request"; import { ProviderOrganizationCreateRequest } from "../admin-console/models/request/provider/provider-organization-create.request"; -import { ProviderSetupRequest } from "../admin-console/models/request/provider/provider-setup.request"; -import { ProviderUpdateRequest } from "../admin-console/models/request/provider/provider-update.request"; import { ProviderUserAcceptRequest } from "../admin-console/models/request/provider/provider-user-accept.request"; import { ProviderUserBulkConfirmRequest } from "../admin-console/models/request/provider/provider-user-bulk-confirm.request"; import { ProviderUserBulkRequest } from "../admin-console/models/request/provider/provider-user-bulk.request"; @@ -32,7 +30,6 @@ import { ProviderUserResponse, ProviderUserUserDetailsResponse, } from "../admin-console/models/response/provider/provider-user.response"; -import { ProviderResponse } from "../admin-console/models/response/provider/provider.response"; import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response"; import { TokenService } from "../auth/abstractions/token.service"; import { CreateAuthRequest } from "../auth/models/request/create-auth.request"; @@ -565,8 +562,12 @@ export class ApiService implements ApiServiceAbstraction { return this.send("PUT", "/ciphers/share", request, true, false); } - putCipherCollections(id: string, request: CipherCollectionsRequest): Promise { - return this.send("PUT", "/ciphers/" + id + "/collections", request, true, false); + async putCipherCollections( + id: string, + request: CipherCollectionsRequest, + ): Promise { + const response = await this.send("PUT", "/ciphers/" + id + "/collections", request, true, true); + return new CipherResponse(response); } putCipherCollectionsAdmin(id: string, request: CipherCollectionsRequest): Promise { @@ -862,16 +863,6 @@ export class ApiService implements ApiServiceAbstraction { return r; } - async putGroupUsers(organizationId: string, id: string, request: string[]): Promise { - await this.send( - "PUT", - "/organizations/" + organizationId + "/groups/" + id + "/users", - request, - true, - false, - ); - } - deleteGroupUser(organizationId: string, id: string, organizationUserId: string): Promise { return this.send( "DELETE", @@ -1157,23 +1148,6 @@ export class ApiService implements ApiServiceAbstraction { return this.send("DELETE", "/organizations/connections/" + id, null, true, false); } - // Provider APIs - - async postProviderSetup(id: string, request: ProviderSetupRequest) { - const r = await this.send("POST", "/providers/" + id + "/setup", request, true, true); - return new ProviderResponse(r); - } - - async getProvider(id: string) { - const r = await this.send("GET", "/providers/" + id, null, true, true); - return new ProviderResponse(r); - } - - async putProvider(id: string, request: ProviderUpdateRequest) { - const r = await this.send("PUT", "/providers/" + id, request, true, true); - return new ProviderResponse(r); - } - // Provider User APIs async getProviderUsers( diff --git a/libs/common/src/services/event/event-collection.service.ts b/libs/common/src/services/event/event-collection.service.ts index 2d2b553062..641c1b4d44 100644 --- a/libs/common/src/services/event/event-collection.service.ts +++ b/libs/common/src/services/event/event-collection.service.ts @@ -3,7 +3,7 @@ import { firstValueFrom, map, from, zip } from "rxjs"; import { EventCollectionService as EventCollectionServiceAbstraction } from "../../abstractions/event/event-collection.service"; import { EventUploadService } from "../../abstractions/event/event-upload.service"; import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; -import { AccountService } from "../../auth/abstractions/account.service"; +import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { EventType } from "../../enums"; import { EventData } from "../../models/data/event.data"; @@ -18,7 +18,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction private stateProvider: StateProvider, private organizationService: OrganizationService, private eventUploadService: EventUploadService, - private accountService: AccountService, + private authService: AuthService, ) {} /** Adds an event to the active user's event collection @@ -71,12 +71,12 @@ export class EventCollectionService implements EventCollectionServiceAbstraction const cipher$ = from(this.cipherService.get(cipherId)); - const [accountInfo, orgIds, cipher] = await firstValueFrom( - zip(this.accountService.activeAccount$, orgIds$, cipher$), + const [authStatus, orgIds, cipher] = await firstValueFrom( + zip(this.authService.activeAccountStatus$, orgIds$, cipher$), ); // The user must be authorized - if (accountInfo.status != AuthenticationStatus.Unlocked) { + if (authStatus != AuthenticationStatus.Unlocked) { return false; } diff --git a/libs/common/src/services/event/event-upload.service.ts b/libs/common/src/services/event/event-upload.service.ts index 4ee4300c39..6f229751bf 100644 --- a/libs/common/src/services/event/event-upload.service.ts +++ b/libs/common/src/services/event/event-upload.service.ts @@ -2,7 +2,7 @@ import { firstValueFrom, map } from "rxjs"; import { ApiService } from "../../abstractions/api.service"; import { EventUploadService as EventUploadServiceAbstraction } from "../../abstractions/event/event-upload.service"; -import { AccountService } from "../../auth/abstractions/account.service"; +import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { EventData } from "../../models/data/event.data"; import { EventRequest } from "../../models/request/event.request"; @@ -18,7 +18,7 @@ export class EventUploadService implements EventUploadServiceAbstraction { private apiService: ApiService, private stateProvider: StateProvider, private logService: LogService, - private accountService: AccountService, + private authService: AuthService, ) {} init(checkOnInterval: boolean) { @@ -43,13 +43,16 @@ export class EventUploadService implements EventUploadServiceAbstraction { userId = await firstValueFrom(this.stateProvider.activeUserId$); } - // Get the auth status from the provided user or the active user - const userAuth$ = this.accountService.accounts$.pipe( - map((accounts) => accounts[userId]?.status === AuthenticationStatus.Unlocked), - ); + if (!userId) { + return; + } - const isAuthenticated = await firstValueFrom(userAuth$); - if (!isAuthenticated) { + const isUnlocked = await firstValueFrom( + this.authService + .authStatusFor$(userId) + .pipe(map((status) => status === AuthenticationStatus.Unlocked)), + ); + if (!isUnlocked) { return; } diff --git a/libs/common/src/services/event/key-definitions.ts b/libs/common/src/services/event/key-definitions.ts index 1059d24b72..5682099688 100644 --- a/libs/common/src/services/event/key-definitions.ts +++ b/libs/common/src/services/event/key-definitions.ts @@ -1,10 +1,11 @@ import { EventData } from "../../models/data/event.data"; -import { KeyDefinition, EVENT_COLLECTION_DISK } from "../../platform/state"; +import { EVENT_COLLECTION_DISK, UserKeyDefinition } from "../../platform/state"; -export const EVENT_COLLECTION: KeyDefinition = KeyDefinition.array( +export const EVENT_COLLECTION = UserKeyDefinition.array( EVENT_COLLECTION_DISK, "events", { deserializer: (s) => EventData.fromJSON(s), + clearOn: ["logout"], }, ); diff --git a/libs/common/src/services/notifications.service.ts b/libs/common/src/services/notifications.service.ts index 4dc8772d00..7dc54b849f 100644 --- a/libs/common/src/services/notifications.service.ts +++ b/libs/common/src/services/notifications.service.ts @@ -2,6 +2,7 @@ import * as signalR from "@microsoft/signalr"; import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack"; import { firstValueFrom } from "rxjs"; +import { AuthRequestServiceAbstraction } from "../../../auth/src/common/abstractions"; import { ApiService } from "../abstractions/api.service"; import { NotificationsService as NotificationsServiceAbstraction } from "../abstractions/notifications.service"; import { AuthService } from "../auth/abstractions/auth.service"; @@ -18,6 +19,7 @@ import { EnvironmentService } from "../platform/abstractions/environment.service import { LogService } from "../platform/abstractions/log.service"; import { MessagingService } from "../platform/abstractions/messaging.service"; import { StateService } from "../platform/abstractions/state.service"; +import { UserId } from "../types/guid"; import { SyncService } from "../vault/abstractions/sync/sync.service.abstraction"; export class NotificationsService implements NotificationsServiceAbstraction { @@ -37,6 +39,7 @@ export class NotificationsService implements NotificationsServiceAbstraction { private logoutCallback: (expired: boolean) => Promise, private stateService: StateService, private authService: AuthService, + private authRequestService: AuthRequestServiceAbstraction, private messagingService: MessagingService, ) { this.environmentService.environment$.subscribe(() => { @@ -199,10 +202,13 @@ export class NotificationsService implements NotificationsServiceAbstraction { await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification); break; case NotificationType.AuthRequest: - if (await this.stateService.getApproveLoginRequests()) { - this.messagingService.send("openLoginApproval", { - notificationId: notification.payload.id, - }); + { + const userId = await this.stateService.getUserId(); + if (await this.authRequestService.getAcceptAuthRequests(userId as UserId)) { + this.messagingService.send("openLoginApproval", { + notificationId: notification.payload.id, + }); + } } break; default: diff --git a/libs/common/src/services/search.service.ts b/libs/common/src/services/search.service.ts index 773d51297a..38ddfe0e47 100644 --- a/libs/common/src/services/search.service.ts +++ b/libs/common/src/services/search.service.ts @@ -1,20 +1,91 @@ import * as lunr from "lunr"; +import { Observable, firstValueFrom, map } from "rxjs"; +import { Jsonify } from "type-fest"; import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service"; import { UriMatchStrategy } from "../models/domain/domain-service"; import { I18nService } from "../platform/abstractions/i18n.service"; import { LogService } from "../platform/abstractions/log.service"; +import { + ActiveUserState, + StateProvider, + UserKeyDefinition, + VAULT_SEARCH_MEMORY, +} from "../platform/state"; import { SendView } from "../tools/send/models/view/send.view"; +import { IndexedEntityId } from "../types/guid"; import { FieldType } from "../vault/enums"; import { CipherType } from "../vault/enums/cipher-type"; import { CipherView } from "../vault/models/view/cipher.view"; +export type SerializedLunrIndex = { + version: string; + fields: string[]; + fieldVectors: [string, number[]]; + invertedIndex: any[]; + pipeline: string[]; +}; + +/** + * The `KeyDefinition` for accessing the search index in application state. + * The key definition is configured to clear the index when the user locks the vault. + */ +export const LUNR_SEARCH_INDEX = new UserKeyDefinition( + VAULT_SEARCH_MEMORY, + "searchIndex", + { + deserializer: (obj: Jsonify) => obj, + clearOn: ["lock", "logout"], + }, +); + +/** + * The `KeyDefinition` for accessing the ID of the entity currently indexed by Lunr search. + * The key definition is configured to clear the indexed entity ID when the user locks the vault. + */ +export const LUNR_SEARCH_INDEXED_ENTITY_ID = new UserKeyDefinition( + VAULT_SEARCH_MEMORY, + "searchIndexedEntityId", + { + deserializer: (obj: Jsonify) => obj, + clearOn: ["lock", "logout"], + }, +); + +/** + * The `KeyDefinition` for accessing the state of Lunr search indexing, indicating whether the Lunr search index is currently being built or updating. + * The key definition is configured to clear the indexing state when the user locks the vault. + */ +export const LUNR_SEARCH_INDEXING = new UserKeyDefinition( + VAULT_SEARCH_MEMORY, + "isIndexing", + { + deserializer: (obj: Jsonify) => obj, + clearOn: ["lock", "logout"], + }, +); + export class SearchService implements SearchServiceAbstraction { private static registeredPipeline = false; - indexedEntityId?: string = null; - private indexing = false; - private index: lunr.Index = null; + private searchIndexState: ActiveUserState = + this.stateProvider.getActive(LUNR_SEARCH_INDEX); + private readonly index$: Observable = this.searchIndexState.state$.pipe( + map((searchIndex) => (searchIndex ? lunr.Index.load(searchIndex) : null)), + ); + + private searchIndexEntityIdState: ActiveUserState = this.stateProvider.getActive( + LUNR_SEARCH_INDEXED_ENTITY_ID, + ); + readonly indexedEntityId$: Observable = + this.searchIndexEntityIdState.state$.pipe(map((id) => id)); + + private searchIsIndexingState: ActiveUserState = + this.stateProvider.getActive(LUNR_SEARCH_INDEXING); + private readonly searchIsIndexing$: Observable = this.searchIsIndexingState.state$.pipe( + map((indexing) => indexing ?? false), + ); + private readonly immediateSearchLocales: string[] = ["zh-CN", "zh-TW", "ja", "ko", "vi"]; private readonly defaultSearchableMinLength: number = 2; private searchableMinLength: number = this.defaultSearchableMinLength; @@ -22,6 +93,7 @@ export class SearchService implements SearchServiceAbstraction { constructor( private logService: LogService, private i18nService: I18nService, + private stateProvider: StateProvider, ) { this.i18nService.locale$.subscribe((locale) => { if (this.immediateSearchLocales.indexOf(locale) !== -1) { @@ -40,28 +112,29 @@ export class SearchService implements SearchServiceAbstraction { } } - clearIndex(): void { - this.indexedEntityId = null; - this.index = null; + async clearIndex(): Promise { + await this.searchIndexEntityIdState.update(() => null); + await this.searchIndexState.update(() => null); + await this.searchIsIndexingState.update(() => null); } - isSearchable(query: string): boolean { + async isSearchable(query: string): Promise { query = SearchService.normalizeSearchQuery(query); + const index = await this.getIndexForSearch(); const notSearchable = query == null || - (this.index == null && query.length < this.searchableMinLength) || - (this.index != null && query.length < this.searchableMinLength && query.indexOf(">") !== 0); + (index == null && query.length < this.searchableMinLength) || + (index != null && query.length < this.searchableMinLength && query.indexOf(">") !== 0); return !notSearchable; } - indexCiphers(ciphers: CipherView[], indexedEntityId?: string): void { - if (this.indexing) { + async indexCiphers(ciphers: CipherView[], indexedEntityId?: string): Promise { + if (await this.getIsIndexing()) { return; } - this.indexing = true; - this.indexedEntityId = indexedEntityId; - this.index = null; + await this.setIsIndexing(true); + await this.setIndexedEntityIdForSearch(indexedEntityId as IndexedEntityId); const builder = new lunr.Builder(); builder.pipeline.add(this.normalizeAccentsPipelineFunction); builder.ref("id"); @@ -95,9 +168,11 @@ export class SearchService implements SearchServiceAbstraction { builder.field("organizationid", { extractor: (c: CipherView) => c.organizationId }); ciphers = ciphers || []; ciphers.forEach((c) => builder.add(c)); - this.index = builder.build(); + const index = builder.build(); - this.indexing = false; + await this.setIndexForSearch(index.toJSON() as SerializedLunrIndex); + + await this.setIsIndexing(false); this.logService.info("Finished search indexing"); } @@ -125,18 +200,18 @@ export class SearchService implements SearchServiceAbstraction { ciphers = ciphers.filter(filter as (cipher: CipherView) => boolean); } - if (!this.isSearchable(query)) { + if (!(await this.isSearchable(query))) { return ciphers; } - if (this.indexing) { + if (await this.getIsIndexing()) { await new Promise((r) => setTimeout(r, 250)); - if (this.indexing) { + if (await this.getIsIndexing()) { await new Promise((r) => setTimeout(r, 500)); } } - const index = this.getIndexForSearch(); + const index = await this.getIndexForSearch(); if (index == null) { // Fall back to basic search if index is not available return this.searchCiphersBasic(ciphers, query); @@ -230,8 +305,24 @@ export class SearchService implements SearchServiceAbstraction { return sendsMatched.concat(lowPriorityMatched); } - getIndexForSearch(): lunr.Index { - return this.index; + async getIndexForSearch(): Promise { + return await firstValueFrom(this.index$); + } + + private async setIndexForSearch(index: SerializedLunrIndex): Promise { + await this.searchIndexState.update(() => index); + } + + private async setIndexedEntityIdForSearch(indexedEntityId: IndexedEntityId): Promise { + await this.searchIndexEntityIdState.update(() => indexedEntityId); + } + + private async setIsIndexing(indexing: boolean): Promise { + await this.searchIsIndexingState.update(() => indexing); + } + + private async getIsIndexing(): Promise { + return await firstValueFrom(this.searchIsIndexing$); } private fieldExtractor(c: CipherView, joined: boolean) { diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts index a8afc63297..4fac3be9c9 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts @@ -172,7 +172,6 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA } async clear(userId?: string): Promise { - await this.stateService.setEverBeenUnlocked(false, { userId: userId }); await this.cryptoService.clearPinKeys(userId); } diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts index e48f2fe0a3..5344093a25 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts @@ -1,17 +1,21 @@ import { MockProxy, any, mock } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; +import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { SearchService } from "../../abstractions/search.service"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; +import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { StateService } from "../../platform/abstractions/state.service"; +import { Utils } from "../../platform/misc/utils"; import { Account } from "../../platform/models/domain/account"; import { StateEventRunnerService } from "../../platform/state"; +import { UserId } from "../../types/guid"; import { CipherService } from "../../vault/abstractions/cipher.service"; import { CollectionService } from "../../vault/abstractions/collection.service"; import { FolderService } from "../../vault/abstractions/folder/folder.service.abstraction"; @@ -19,6 +23,8 @@ import { FolderService } from "../../vault/abstractions/folder/folder.service.ab import { VaultTimeoutService } from "./vault-timeout.service"; describe("VaultTimeoutService", () => { + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; let cipherService: MockProxy; let folderService: MockProxy; let collectionService: MockProxy; @@ -39,7 +45,11 @@ describe("VaultTimeoutService", () => { let vaultTimeoutService: VaultTimeoutService; + const userId = Utils.newGuid() as UserId; + beforeEach(() => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); cipherService = mock(); folderService = mock(); collectionService = mock(); @@ -66,6 +76,8 @@ describe("VaultTimeoutService", () => { availableVaultTimeoutActionsSubject = new BehaviorSubject([]); vaultTimeoutService = new VaultTimeoutService( + accountService, + masterPasswordService, cipherService, folderService, collectionService, @@ -123,6 +135,14 @@ describe("VaultTimeoutService", () => { stateService.activeAccount$ = new BehaviorSubject(globalSetups?.userId); + if (globalSetups?.userId) { + accountService.activeAccountSubject.next({ + id: globalSetups.userId as UserId, + email: null, + name: null, + }); + } + platformUtilsService.isViewOpen.mockResolvedValue(globalSetups?.isViewOpen ?? false); vaultTimeoutSettingsService.vaultTimeoutAction$.mockImplementation((userId) => { @@ -154,10 +174,8 @@ describe("VaultTimeoutService", () => { // This does NOT assert all the things that the lock process does expect(stateService.getIsAuthenticated).toHaveBeenCalledWith({ userId: userId }); expect(vaultTimeoutSettingsService.availableVaultTimeoutActions$).toHaveBeenCalledWith(userId); - expect(stateService.setEverBeenUnlocked).toHaveBeenCalledWith(true, { userId: userId }); expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(null, { userId: userId }); - expect(cryptoService.clearUserKey).toHaveBeenCalledWith(false, userId); - expect(cryptoService.clearMasterKey).toHaveBeenCalledWith(userId); + expect(masterPasswordService.mock.clearMasterKey).toHaveBeenCalledWith(userId); expect(cipherService.clearCache).toHaveBeenCalledWith(userId); expect(lockedCallback).toHaveBeenCalledWith(userId); }; diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.ts index c3270ac2b8..8baf6c04c4 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -3,7 +3,9 @@ import { firstValueFrom, timeout } from "rxjs"; import { SearchService } from "../../abstractions/search.service"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout.service"; +import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service"; +import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { ClientType } from "../../enums"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; @@ -21,6 +23,8 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { private inited = false; constructor( + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cipherService: CipherService, private folderService: FolderService, private collectionService: CollectionService, @@ -84,23 +88,19 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { await this.logOut(userId); } - const currentUserId = await this.stateService.getUserId(); + const currentUserId = (await firstValueFrom(this.accountService.activeAccount$)).id; if (userId == null || userId === currentUserId) { - this.searchService.clearIndex(); + await this.searchService.clearIndex(); await this.folderService.clearCache(); await this.collectionService.clearActiveUserCache(); } - await this.stateService.setEverBeenUnlocked(true, { userId: userId }); + await this.masterPasswordService.clearMasterKey((userId ?? currentUserId) as UserId); + await this.stateService.setUserKeyAutoUnlock(null, { userId: userId }); await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId }); - await this.cryptoService.clearUserKey(false, userId); - await this.cryptoService.clearMasterKey(userId); - await this.cryptoService.clearOrgKeys(true, userId); - await this.cryptoService.clearKeyPair(true, userId); - await this.cipherService.clearCache(userId); await this.stateEventRunnerService.handleEvent("lock", (userId ?? currentUserId) as UserId); diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index faccddb0af..f9a8734731 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -51,6 +51,10 @@ import { RememberedEmailMigrator } from "./migrations/51-move-remembered-email-t import { DeleteInstalledVersion } from "./migrations/52-delete-installed-version"; import { DeviceTrustCryptoServiceStateProviderMigrator } from "./migrations/53-migrate-device-trust-crypto-svc-to-state-providers"; import { SendMigrator } from "./migrations/54-move-encrypted-sends"; +import { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-master-key-state-to-provider"; +import { AuthRequestMigrator } from "./migrations/56-move-auth-requests"; +import { CipherServiceMigrator } from "./migrations/57-move-cipher-service-to-state-provider"; +import { RemoveRefreshTokenMigratedFlagMigrator } from "./migrations/58-remove-refresh-token-migrated-state-provider-flag"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; @@ -58,8 +62,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 54; - +export const CURRENT_VERSION = 58; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -115,7 +118,11 @@ export function createMigrationBuilder() { .with(RememberedEmailMigrator, 50, 51) .with(DeleteInstalledVersion, 51, 52) .with(DeviceTrustCryptoServiceStateProviderMigrator, 52, 53) - .with(SendMigrator, 53, 54); + .with(SendMigrator, 53, 54) + .with(MoveMasterKeyStateToProviderMigrator, 54, 55) + .with(AuthRequestMigrator, 55, 56) + .with(CipherServiceMigrator, 56, 57) + .with(RemoveRefreshTokenMigratedFlagMigrator, 57, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.spec.ts b/libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.spec.ts new file mode 100644 index 0000000000..bbf0352e95 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.spec.ts @@ -0,0 +1,210 @@ +import { any, MockProxy } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { + FORCE_SET_PASSWORD_REASON_DEFINITION, + MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION, + MASTER_KEY_HASH_DEFINITION, + MoveMasterKeyStateToProviderMigrator, +} from "./55-move-master-key-state-to-provider"; + +function preMigrationState() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount", "ThirdAccount"], + // prettier-ignore + "FirstAccount": { + profile: { + forceSetPasswordReason: "FirstAccount_forceSetPasswordReason", + keyHash: "FirstAccount_keyHash", + otherStuff: "overStuff2", + }, + keys: { + masterKeyEncryptedUserKey: "FirstAccount_masterKeyEncryptedUserKey", + }, + otherStuff: "otherStuff3", + }, + // prettier-ignore + "SecondAccount": { + profile: { + forceSetPasswordReason: "SecondAccount_forceSetPasswordReason", + keyHash: "SecondAccount_keyHash", + otherStuff: "otherStuff4", + }, + keys: { + masterKeyEncryptedUserKey: "SecondAccount_masterKeyEncryptedUserKey", + }, + otherStuff: "otherStuff5", + }, + // prettier-ignore + "ThirdAccount": { + profile: { + otherStuff: "otherStuff6", + }, + }, + }; +} + +function postMigrationState() { + return { + user_FirstAccount_masterPassword_forceSetPasswordReason: "FirstAccount_forceSetPasswordReason", + user_FirstAccount_masterPassword_masterKeyHash: "FirstAccount_keyHash", + user_FirstAccount_masterPassword_masterKeyEncryptedUserKey: + "FirstAccount_masterKeyEncryptedUserKey", + user_SecondAccount_masterPassword_forceSetPasswordReason: + "SecondAccount_forceSetPasswordReason", + user_SecondAccount_masterPassword_masterKeyHash: "SecondAccount_keyHash", + user_SecondAccount_masterPassword_masterKeyEncryptedUserKey: + "SecondAccount_masterKeyEncryptedUserKey", + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount"], + // prettier-ignore + "FirstAccount": { + profile: { + otherStuff: "overStuff2", + }, + otherStuff: "otherStuff3", + }, + // prettier-ignore + "SecondAccount": { + profile: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + // prettier-ignore + "ThirdAccount": { + profile: { + otherStuff: "otherStuff6", + }, + }, + }; +} + +describe("MoveForceSetPasswordReasonToStateProviderMigrator", () => { + let helper: MockProxy; + let sut: MoveMasterKeyStateToProviderMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(preMigrationState(), 54); + sut = new MoveMasterKeyStateToProviderMigrator(54, 55); + }); + + it("should remove properties from existing accounts", async () => { + await sut.migrate(helper); + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + profile: { + otherStuff: "overStuff2", + }, + keys: {}, + otherStuff: "otherStuff3", + }); + expect(helper.set).toHaveBeenCalledWith("SecondAccount", { + profile: { + otherStuff: "otherStuff4", + }, + keys: {}, + otherStuff: "otherStuff5", + }); + }); + + it("should set properties for each account", async () => { + await sut.migrate(helper); + + expect(helper.setToUser).toHaveBeenCalledWith( + "FirstAccount", + FORCE_SET_PASSWORD_REASON_DEFINITION, + "FirstAccount_forceSetPasswordReason", + ); + + expect(helper.setToUser).toHaveBeenCalledWith( + "FirstAccount", + MASTER_KEY_HASH_DEFINITION, + "FirstAccount_keyHash", + ); + + expect(helper.setToUser).toHaveBeenCalledWith( + "FirstAccount", + MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION, + "FirstAccount_masterKeyEncryptedUserKey", + ); + + expect(helper.setToUser).toHaveBeenCalledWith( + "SecondAccount", + FORCE_SET_PASSWORD_REASON_DEFINITION, + "SecondAccount_forceSetPasswordReason", + ); + + expect(helper.setToUser).toHaveBeenCalledWith( + "SecondAccount", + MASTER_KEY_HASH_DEFINITION, + "SecondAccount_keyHash", + ); + + expect(helper.setToUser).toHaveBeenCalledWith( + "SecondAccount", + MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION, + "SecondAccount_masterKeyEncryptedUserKey", + ); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(postMigrationState(), 55); + sut = new MoveMasterKeyStateToProviderMigrator(54, 55); + }); + + it.each(["FirstAccount", "SecondAccount"])("should null out new values", async (userId) => { + await sut.rollback(helper); + + expect(helper.setToUser).toHaveBeenCalledWith( + userId, + FORCE_SET_PASSWORD_REASON_DEFINITION, + null, + ); + + expect(helper.setToUser).toHaveBeenCalledWith(userId, MASTER_KEY_HASH_DEFINITION, null); + }); + + it("should add explicit value back to accounts", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + profile: { + forceSetPasswordReason: "FirstAccount_forceSetPasswordReason", + keyHash: "FirstAccount_keyHash", + otherStuff: "overStuff2", + }, + keys: { + masterKeyEncryptedUserKey: "FirstAccount_masterKeyEncryptedUserKey", + }, + otherStuff: "otherStuff3", + }); + expect(helper.set).toHaveBeenCalledWith("SecondAccount", { + profile: { + forceSetPasswordReason: "SecondAccount_forceSetPasswordReason", + keyHash: "SecondAccount_keyHash", + otherStuff: "otherStuff4", + }, + keys: { + masterKeyEncryptedUserKey: "SecondAccount_masterKeyEncryptedUserKey", + }, + otherStuff: "otherStuff5", + }); + }); + + it("should not try to restore values to missing accounts", async () => { + await sut.rollback(helper); + + expect(helper.set).not.toHaveBeenCalledWith("ThirdAccount", any()); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.ts b/libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.ts new file mode 100644 index 0000000000..99b22b5661 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.ts @@ -0,0 +1,111 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +type ExpectedAccountType = { + keys?: { + masterKeyEncryptedUserKey?: string; + }; + profile?: { + forceSetPasswordReason?: number; + keyHash?: string; + }; +}; + +export const FORCE_SET_PASSWORD_REASON_DEFINITION: KeyDefinitionLike = { + key: "forceSetPasswordReason", + stateDefinition: { + name: "masterPassword", + }, +}; + +export const MASTER_KEY_HASH_DEFINITION: KeyDefinitionLike = { + key: "masterKeyHash", + stateDefinition: { + name: "masterPassword", + }, +}; + +export const MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION: KeyDefinitionLike = { + key: "masterKeyEncryptedUserKey", + stateDefinition: { + name: "masterPassword", + }, +}; + +export class MoveMasterKeyStateToProviderMigrator extends Migrator<54, 55> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { + const forceSetPasswordReason = account?.profile?.forceSetPasswordReason; + if (forceSetPasswordReason != null) { + await helper.setToUser( + userId, + FORCE_SET_PASSWORD_REASON_DEFINITION, + forceSetPasswordReason, + ); + + delete account.profile.forceSetPasswordReason; + await helper.set(userId, account); + } + + const masterKeyHash = account?.profile?.keyHash; + if (masterKeyHash != null) { + await helper.setToUser(userId, MASTER_KEY_HASH_DEFINITION, masterKeyHash); + + delete account.profile.keyHash; + await helper.set(userId, account); + } + + const masterKeyEncryptedUserKey = account?.keys?.masterKeyEncryptedUserKey; + if (masterKeyEncryptedUserKey != null) { + await helper.setToUser( + userId, + MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION, + masterKeyEncryptedUserKey, + ); + + delete account.keys.masterKeyEncryptedUserKey; + await helper.set(userId, account); + } + } + + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + } + async rollback(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise { + const forceSetPasswordReason = await helper.getFromUser( + userId, + FORCE_SET_PASSWORD_REASON_DEFINITION, + ); + const masterKeyHash = await helper.getFromUser(userId, MASTER_KEY_HASH_DEFINITION); + const masterKeyEncryptedUserKey = await helper.getFromUser( + userId, + MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION, + ); + if (account != null) { + if (forceSetPasswordReason != null) { + account.profile = Object.assign(account.profile ?? {}, { + forceSetPasswordReason, + }); + } + if (masterKeyHash != null) { + account.profile = Object.assign(account.profile ?? {}, { + keyHash: masterKeyHash, + }); + } + if (masterKeyEncryptedUserKey != null) { + account.keys = Object.assign(account.keys ?? {}, { + masterKeyEncryptedUserKey, + }); + } + await helper.set(userId, account); + } + + await helper.setToUser(userId, FORCE_SET_PASSWORD_REASON_DEFINITION, null); + await helper.setToUser(userId, MASTER_KEY_HASH_DEFINITION, null); + } + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + } +} diff --git a/libs/common/src/state-migrations/migrations/56-move-auth-requests.spec.ts b/libs/common/src/state-migrations/migrations/56-move-auth-requests.spec.ts new file mode 100644 index 0000000000..f6bddbce7d --- /dev/null +++ b/libs/common/src/state-migrations/migrations/56-move-auth-requests.spec.ts @@ -0,0 +1,138 @@ +import { MockProxy } from "jest-mock-extended"; + +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { AuthRequestMigrator } from "./56-move-auth-requests"; + +function exampleJSON() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount"], + FirstAccount: { + settings: { + otherStuff: "otherStuff2", + approveLoginRequests: true, + }, + otherStuff: "otherStuff3", + adminAuthRequest: { + id: "id1", + privateKey: "privateKey1", + }, + }, + SecondAccount: { + settings: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +function rollbackJSON() { + return { + user_FirstAccount_authRequestLocal_adminAuthRequest: { + id: "id1", + privateKey: "privateKey1", + }, + user_FirstAccount_authRequestLocal_acceptAuthRequests: true, + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount"], + FirstAccount: { + settings: { + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }, + SecondAccount: { + settings: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +const ADMIN_AUTH_REQUEST_KEY: KeyDefinitionLike = { + stateDefinition: { + name: "authRequestLocal", + }, + key: "adminAuthRequest", +}; + +const ACCEPT_AUTH_REQUESTS_KEY: KeyDefinitionLike = { + stateDefinition: { + name: "authRequestLocal", + }, + key: "acceptAuthRequests", +}; + +describe("AuthRequestMigrator", () => { + let helper: MockProxy; + let sut: AuthRequestMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(exampleJSON(), 55); + sut = new AuthRequestMigrator(55, 56); + }); + + it("removes the existing adminAuthRequest and approveLoginRequests", async () => { + await sut.migrate(helper); + + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + settings: { + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }); + expect(helper.set).not.toHaveBeenCalledWith("SecondAccount"); + }); + + it("sets the adminAuthRequest and approveLoginRequests under the new key definitions", async () => { + await sut.migrate(helper); + + expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", ADMIN_AUTH_REQUEST_KEY, { + id: "id1", + privateKey: "privateKey1", + }); + + expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", ACCEPT_AUTH_REQUESTS_KEY, true); + expect(helper.setToUser).not.toHaveBeenCalledWith("SecondAccount"); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 56); + sut = new AuthRequestMigrator(55, 56); + }); + + it("nulls the new adminAuthRequest and acceptAuthRequests values", async () => { + await sut.rollback(helper); + + expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", ADMIN_AUTH_REQUEST_KEY, null); + expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", ACCEPT_AUTH_REQUESTS_KEY, null); + }); + + it("sets back the adminAuthRequest and approveLoginRequests under old account object", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + adminAuthRequest: { + id: "id1", + privateKey: "privateKey1", + }, + settings: { + otherStuff: "otherStuff2", + approveLoginRequests: true, + }, + otherStuff: "otherStuff3", + }); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/56-move-auth-requests.ts b/libs/common/src/state-migrations/migrations/56-move-auth-requests.ts new file mode 100644 index 0000000000..4fec3b2de0 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/56-move-auth-requests.ts @@ -0,0 +1,104 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +type AdminAuthRequestStorable = { + id: string; + privateKey: string; +}; + +type ExpectedAccountType = { + adminAuthRequest?: AdminAuthRequestStorable; + settings?: { + approveLoginRequests?: boolean; + }; +}; + +const ADMIN_AUTH_REQUEST_KEY: KeyDefinitionLike = { + stateDefinition: { + name: "authRequestLocal", + }, + key: "adminAuthRequest", +}; + +const ACCEPT_AUTH_REQUESTS_KEY: KeyDefinitionLike = { + stateDefinition: { + name: "authRequestLocal", + }, + key: "acceptAuthRequests", +}; + +export class AuthRequestMigrator extends Migrator<55, 56> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { + let updatedAccount = false; + + // Migrate admin auth request + const existingAdminAuthRequest = account?.adminAuthRequest; + + if (existingAdminAuthRequest != null) { + await helper.setToUser(userId, ADMIN_AUTH_REQUEST_KEY, existingAdminAuthRequest); + delete account.adminAuthRequest; + updatedAccount = true; + } + + // Migrate approve login requests + const existingApproveLoginRequests = account?.settings?.approveLoginRequests; + + if (existingApproveLoginRequests != null) { + await helper.setToUser(userId, ACCEPT_AUTH_REQUESTS_KEY, existingApproveLoginRequests); + delete account.settings.approveLoginRequests; + updatedAccount = true; + } + + if (updatedAccount) { + // Save the migrated account + await helper.set(userId, account); + } + } + + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + } + + async rollback(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise { + let updatedAccount = false; + // Rollback admin auth request + const migratedAdminAuthRequest: AdminAuthRequestStorable = await helper.getFromUser( + userId, + ADMIN_AUTH_REQUEST_KEY, + ); + + if (migratedAdminAuthRequest != null) { + account.adminAuthRequest = migratedAdminAuthRequest; + updatedAccount = true; + } + + await helper.setToUser(userId, ADMIN_AUTH_REQUEST_KEY, null); + + // Rollback approve login requests + const migratedAcceptAuthRequest: boolean = await helper.getFromUser( + userId, + ACCEPT_AUTH_REQUESTS_KEY, + ); + + if (migratedAcceptAuthRequest != null) { + account.settings = Object.assign(account.settings ?? {}, { + approveLoginRequests: migratedAcceptAuthRequest, + }); + updatedAccount = true; + } + + await helper.setToUser(userId, ACCEPT_AUTH_REQUESTS_KEY, null); + + if (updatedAccount) { + await helper.set(userId, account); + } + } + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + } +} diff --git a/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.spec.ts new file mode 100644 index 0000000000..f51699bc79 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.spec.ts @@ -0,0 +1,174 @@ +import { MockProxy, any } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { + CIPHERS_DISK, + CIPHERS_DISK_LOCAL, + CipherServiceMigrator, +} from "./57-move-cipher-service-to-state-provider"; + +function exampleJSON() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["user1", "user2"], + user1: { + data: { + localData: { + "6865ba55-7966-4d63-b743-b12000d49631": { + lastUsedDate: 1708950970632, + }, + "f895f099-6739-4cca-9d61-b12200d04bfa": { + lastUsedDate: 1709031916943, + }, + }, + ciphers: { + encrypted: { + "cipher-id-10": { + id: "cipher-id-10", + }, + "cipher-id-11": { + id: "cipher-id-11", + }, + }, + }, + }, + }, + user2: { + data: { + otherStuff: "otherStuff5", + }, + }, + }; +} + +function rollbackJSON() { + return { + user_user1_ciphersLocal_localData: { + "6865ba55-7966-4d63-b743-b12000d49631": { + lastUsedDate: 1708950970632, + }, + "f895f099-6739-4cca-9d61-b12200d04bfa": { + lastUsedDate: 1709031916943, + }, + }, + user_user1_ciphers_ciphers: { + "cipher-id-10": { + id: "cipher-id-10", + }, + "cipher-id-11": { + id: "cipher-id-11", + }, + }, + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["user1", "user2"], + user1: { + data: {}, + }, + user2: { + data: { + localData: { + otherStuff: "otherStuff3", + }, + ciphers: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }, + }; +} + +describe("CipherServiceMigrator", () => { + let helper: MockProxy; + let sut: CipherServiceMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(exampleJSON(), 56); + sut = new CipherServiceMigrator(56, 57); + }); + + it("should remove local data and ciphers from all accounts", async () => { + await sut.migrate(helper); + expect(helper.set).toHaveBeenCalledWith("user1", { + data: {}, + }); + }); + + it("should migrate localData and ciphers to state provider for accounts that have the data", async () => { + await sut.migrate(helper); + + expect(helper.setToUser).toHaveBeenCalledWith("user1", CIPHERS_DISK_LOCAL, { + "6865ba55-7966-4d63-b743-b12000d49631": { + lastUsedDate: 1708950970632, + }, + "f895f099-6739-4cca-9d61-b12200d04bfa": { + lastUsedDate: 1709031916943, + }, + }); + expect(helper.setToUser).toHaveBeenCalledWith("user1", CIPHERS_DISK, { + "cipher-id-10": { + id: "cipher-id-10", + }, + "cipher-id-11": { + id: "cipher-id-11", + }, + }); + + expect(helper.setToUser).not.toHaveBeenCalledWith("user2", CIPHERS_DISK_LOCAL, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user2", CIPHERS_DISK, any()); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 57); + sut = new CipherServiceMigrator(56, 57); + }); + + it.each(["user1", "user2"])("should null out new values", async (userId) => { + await sut.rollback(helper); + expect(helper.setToUser).toHaveBeenCalledWith(userId, CIPHERS_DISK_LOCAL, null); + expect(helper.setToUser).toHaveBeenCalledWith(userId, CIPHERS_DISK, null); + }); + + it("should add back localData and ciphers to all accounts", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledWith("user1", { + data: { + localData: { + "6865ba55-7966-4d63-b743-b12000d49631": { + lastUsedDate: 1708950970632, + }, + "f895f099-6739-4cca-9d61-b12200d04bfa": { + lastUsedDate: 1709031916943, + }, + }, + ciphers: { + encrypted: { + "cipher-id-10": { + id: "cipher-id-10", + }, + "cipher-id-11": { + id: "cipher-id-11", + }, + }, + }, + }, + }); + }); + + it("should not add data back if data wasn't migrated or acct doesn't exist", async () => { + await sut.rollback(helper); + + expect(helper.set).not.toHaveBeenCalledWith("user2", any()); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.ts b/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.ts new file mode 100644 index 0000000000..80c776e1b6 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.ts @@ -0,0 +1,82 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +type ExpectedAccountType = { + data: { + localData?: unknown; + ciphers?: { + encrypted: unknown; + }; + }; +}; + +export const CIPHERS_DISK_LOCAL: KeyDefinitionLike = { + key: "localData", + stateDefinition: { + name: "ciphersLocal", + }, +}; + +export const CIPHERS_DISK: KeyDefinitionLike = { + key: "ciphers", + stateDefinition: { + name: "ciphers", + }, +}; + +export class CipherServiceMigrator extends Migrator<56, 57> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { + let updatedAccount = false; + + //Migrate localData + const localData = account?.data?.localData; + if (localData != null) { + await helper.setToUser(userId, CIPHERS_DISK_LOCAL, localData); + delete account.data.localData; + updatedAccount = true; + } + + //Migrate ciphers + const ciphers = account?.data?.ciphers?.encrypted; + if (ciphers != null) { + await helper.setToUser(userId, CIPHERS_DISK, ciphers); + delete account.data.ciphers; + updatedAccount = true; + } + + if (updatedAccount) { + await helper.set(userId, account); + } + } + + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + } + + async rollback(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise { + //rollback localData + const localData = await helper.getFromUser(userId, CIPHERS_DISK_LOCAL); + + if (account.data && localData != null) { + account.data.localData = localData; + await helper.set(userId, account); + } + await helper.setToUser(userId, CIPHERS_DISK_LOCAL, null); + + //rollback ciphers + const ciphers = await helper.getFromUser(userId, CIPHERS_DISK); + + if (account.data && ciphers != null) { + account.data.ciphers ||= { encrypted: null }; + account.data.ciphers.encrypted = ciphers; + await helper.set(userId, account); + } + await helper.setToUser(userId, CIPHERS_DISK, null); + } + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + } +} diff --git a/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.spec.ts b/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.spec.ts new file mode 100644 index 0000000000..8d8040e4a0 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.spec.ts @@ -0,0 +1,72 @@ +import { MockProxy, any } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; +import { IRREVERSIBLE } from "../migrator"; + +import { + REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE, + RemoveRefreshTokenMigratedFlagMigrator, +} from "./58-remove-refresh-token-migrated-state-provider-flag"; + +// Represents data in state service pre-migration +function preMigrationJson() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["user1", "user2", "user3"], + + user_user1_token_refreshTokenMigratedToSecureStorage: true, + user_user2_token_refreshTokenMigratedToSecureStorage: false, + }; +} + +function rollbackJSON() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["user1", "user2", "user3"], + }; +} + +describe("RemoveRefreshTokenMigratedFlagMigrator", () => { + let helper: MockProxy; + let sut: RemoveRefreshTokenMigratedFlagMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(preMigrationJson(), 57); + sut = new RemoveRefreshTokenMigratedFlagMigrator(57, 58); + }); + + it("should remove refreshTokenMigratedToSecureStorage from state provider for all accounts that have it", async () => { + await sut.migrate(helper); + + expect(helper.removeFromUser).toHaveBeenCalledWith( + "user1", + REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE, + ); + expect(helper.removeFromUser).toHaveBeenCalledWith( + "user2", + REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE, + ); + + expect(helper.removeFromUser).toHaveBeenCalledTimes(2); + + expect(helper.removeFromUser).not.toHaveBeenCalledWith("user3", any()); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 58); + sut = new RemoveRefreshTokenMigratedFlagMigrator(57, 58); + }); + + it("should not add data back and throw IRREVERSIBLE error on call", async () => { + await expect(sut.rollback(helper)).rejects.toThrow(IRREVERSIBLE); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.ts b/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.ts new file mode 100644 index 0000000000..9c6d3776fe --- /dev/null +++ b/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.ts @@ -0,0 +1,34 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { IRREVERSIBLE, Migrator } from "../migrator"; + +type ExpectedAccountType = NonNullable; + +export const REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE: KeyDefinitionLike = { + key: "refreshTokenMigratedToSecureStorage", // matches KeyDefinition.key in DeviceTrustCryptoService + stateDefinition: { + name: "token", // matches StateDefinition.name in StateDefinitions + }, +}; + +export class RemoveRefreshTokenMigratedFlagMigrator extends Migrator<57, 58> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { + const refreshTokenMigratedFlag = await helper.getFromUser( + userId, + REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE, + ); + + if (refreshTokenMigratedFlag != null) { + // Only delete the flag if it exists + await helper.removeFromUser(userId, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE); + } + } + + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + } + + async rollback(helper: MigrationHelper): Promise { + throw IRREVERSIBLE; + } +} diff --git a/libs/common/src/tools/generator/key-definitions.ts b/libs/common/src/tools/generator/key-definitions.ts index 2f35169612..074df48468 100644 --- a/libs/common/src/tools/generator/key-definitions.ts +++ b/libs/common/src/tools/generator/key-definitions.ts @@ -1,4 +1,4 @@ -import { GENERATOR_DISK, GENERATOR_MEMORY, KeyDefinition } from "../../platform/state"; +import { GENERATOR_DISK, GENERATOR_MEMORY, UserKeyDefinition } from "../../platform/state"; import { GeneratedCredential } from "./history/generated-credential"; import { GeneratorNavigation } from "./navigation/generator-navigation"; @@ -17,110 +17,122 @@ import { import { SubaddressGenerationOptions } from "./username/subaddress-generator-options"; /** plaintext password generation options */ -export const GENERATOR_SETTINGS = new KeyDefinition( +export const GENERATOR_SETTINGS = new UserKeyDefinition( GENERATOR_MEMORY, "generatorSettings", { deserializer: (value) => value, + clearOn: ["lock", "logout"], }, ); /** plaintext password generation options */ -export const PASSWORD_SETTINGS = new KeyDefinition( +export const PASSWORD_SETTINGS = new UserKeyDefinition( GENERATOR_DISK, "passwordGeneratorSettings", { deserializer: (value) => value, + clearOn: [], }, ); /** plaintext passphrase generation options */ -export const PASSPHRASE_SETTINGS = new KeyDefinition( +export const PASSPHRASE_SETTINGS = new UserKeyDefinition( GENERATOR_DISK, "passphraseGeneratorSettings", { deserializer: (value) => value, + clearOn: [], }, ); /** plaintext username generation options */ -export const EFF_USERNAME_SETTINGS = new KeyDefinition( +export const EFF_USERNAME_SETTINGS = new UserKeyDefinition( GENERATOR_DISK, "effUsernameGeneratorSettings", { deserializer: (value) => value, + clearOn: [], }, ); /** plaintext configuration for a domain catch-all address. */ -export const CATCHALL_SETTINGS = new KeyDefinition( +export const CATCHALL_SETTINGS = new UserKeyDefinition( GENERATOR_DISK, "catchallGeneratorSettings", { deserializer: (value) => value, + clearOn: [], }, ); /** plaintext configuration for an email subaddress. */ -export const SUBADDRESS_SETTINGS = new KeyDefinition( +export const SUBADDRESS_SETTINGS = new UserKeyDefinition( GENERATOR_DISK, "subaddressGeneratorSettings", { deserializer: (value) => value, + clearOn: [], }, ); /** backing store configuration for {@link Forwarders.AddyIo} */ -export const ADDY_IO_FORWARDER = new KeyDefinition( +export const ADDY_IO_FORWARDER = new UserKeyDefinition( GENERATOR_DISK, "addyIoForwarder", { deserializer: (value) => value, + clearOn: [], }, ); /** backing store configuration for {@link Forwarders.DuckDuckGo} */ -export const DUCK_DUCK_GO_FORWARDER = new KeyDefinition( +export const DUCK_DUCK_GO_FORWARDER = new UserKeyDefinition( GENERATOR_DISK, "duckDuckGoForwarder", { deserializer: (value) => value, + clearOn: [], }, ); /** backing store configuration for {@link Forwarders.FastMail} */ -export const FASTMAIL_FORWARDER = new KeyDefinition( +export const FASTMAIL_FORWARDER = new UserKeyDefinition( GENERATOR_DISK, "fastmailForwarder", { deserializer: (value) => value, + clearOn: [], }, ); /** backing store configuration for {@link Forwarders.FireFoxRelay} */ -export const FIREFOX_RELAY_FORWARDER = new KeyDefinition( +export const FIREFOX_RELAY_FORWARDER = new UserKeyDefinition( GENERATOR_DISK, "firefoxRelayForwarder", { deserializer: (value) => value, + clearOn: [], }, ); /** backing store configuration for {@link Forwarders.ForwardEmail} */ -export const FORWARD_EMAIL_FORWARDER = new KeyDefinition( +export const FORWARD_EMAIL_FORWARDER = new UserKeyDefinition( GENERATOR_DISK, "forwardEmailForwarder", { deserializer: (value) => value, + clearOn: [], }, ); /** backing store configuration for {@link forwarders.SimpleLogin} */ -export const SIMPLE_LOGIN_FORWARDER = new KeyDefinition( +export const SIMPLE_LOGIN_FORWARDER = new UserKeyDefinition( GENERATOR_DISK, "simpleLoginForwarder", { deserializer: (value) => value, + clearOn: [], }, ); @@ -131,5 +143,6 @@ export const GENERATOR_HISTORY = SecretKeyDefinition.array( SecretClassifier.allSecret(), { deserializer: GeneratedCredential.fromJSON, + clearOn: ["logout"], }, ); diff --git a/libs/common/src/tools/generator/state/buffered-key-definition.spec.ts b/libs/common/src/tools/generator/state/buffered-key-definition.spec.ts new file mode 100644 index 0000000000..b056cba397 --- /dev/null +++ b/libs/common/src/tools/generator/state/buffered-key-definition.spec.ts @@ -0,0 +1,119 @@ +import { GENERATOR_DISK, UserKeyDefinition } from "../../../platform/state"; + +import { BufferedKeyDefinition } from "./buffered-key-definition"; + +describe("BufferedKeyDefinition", () => { + const deserializer = (jsonValue: number) => jsonValue + 1; + + describe("toKeyDefinition", () => { + it("should create a key definition", () => { + const key = new BufferedKeyDefinition(GENERATOR_DISK, "test", { + deserializer, + cleanupDelayMs: 5, + clearOn: [], + }); + + const result = key.toKeyDefinition(); + + expect(result).toBeInstanceOf(UserKeyDefinition); + expect(result.stateDefinition).toBe(GENERATOR_DISK); + expect(result.key).toBe("test"); + expect(result.deserializer(1)).toEqual(2); + expect(result.cleanupDelayMs).toEqual(5); + }); + }); + + describe("shouldOverwrite", () => { + it("should call the shouldOverwrite function when its defined", async () => { + const shouldOverwrite = jest.fn(() => true); + const key = new BufferedKeyDefinition(GENERATOR_DISK, "test", { + deserializer, + shouldOverwrite, + clearOn: [], + }); + + const result = await key.shouldOverwrite(true); + + expect(shouldOverwrite).toHaveBeenCalledWith(true); + expect(result).toStrictEqual(true); + }); + + it("should return true when shouldOverwrite is not defined and the input is truthy", async () => { + const key = new BufferedKeyDefinition(GENERATOR_DISK, "test", { + deserializer, + clearOn: [], + }); + + const result = await key.shouldOverwrite(1); + + expect(result).toStrictEqual(true); + }); + + it("should return false when shouldOverwrite is not defined and the input is falsy", async () => { + const key = new BufferedKeyDefinition(GENERATOR_DISK, "test", { + deserializer, + clearOn: [], + }); + + const result = await key.shouldOverwrite(0); + + expect(result).toStrictEqual(false); + }); + }); + + describe("map", () => { + it("should call the map function when its defined", async () => { + const map = jest.fn((value: number) => Promise.resolve(`${value}`)); + const key = new BufferedKeyDefinition(GENERATOR_DISK, "test", { + deserializer, + map, + clearOn: [], + }); + + const result = await key.map(1, true); + + expect(map).toHaveBeenCalledWith(1, true); + expect(result).toStrictEqual("1"); + }); + + it("should fall back to an identity function when map is not defined", async () => { + const key = new BufferedKeyDefinition(GENERATOR_DISK, "test", { deserializer, clearOn: [] }); + + const result = await key.map(1, null); + + expect(result).toStrictEqual(1); + }); + }); + + describe("isValid", () => { + it("should call the isValid function when its defined", async () => { + const isValid = jest.fn(() => Promise.resolve(true)); + const key = new BufferedKeyDefinition(GENERATOR_DISK, "test", { + deserializer, + isValid, + clearOn: [], + }); + + const result = await key.isValid(1, true); + + expect(isValid).toHaveBeenCalledWith(1, true); + expect(result).toStrictEqual(true); + }); + + it("should return true when isValid is not defined and the input is truthy", async () => { + const key = new BufferedKeyDefinition(GENERATOR_DISK, "test", { deserializer, clearOn: [] }); + + const result = await key.isValid(1, null); + + expect(result).toStrictEqual(true); + }); + + it("should return false when isValid is not defined and the input is falsy", async () => { + const key = new BufferedKeyDefinition(GENERATOR_DISK, "test", { deserializer, clearOn: [] }); + + const result = await key.isValid(0, null); + + expect(result).toStrictEqual(false); + }); + }); +}); diff --git a/libs/common/src/tools/generator/state/buffered-key-definition.ts b/libs/common/src/tools/generator/state/buffered-key-definition.ts new file mode 100644 index 0000000000..1f11280839 --- /dev/null +++ b/libs/common/src/tools/generator/state/buffered-key-definition.ts @@ -0,0 +1,104 @@ +import { UserKeyDefinition, UserKeyDefinitionOptions } from "../../../platform/state"; +// eslint-disable-next-line -- `StateDefinition` used as an argument +import { StateDefinition } from "../../../platform/state/state-definition"; + +/** A set of options for customizing the behavior of a {@link BufferedKeyDefinition} + */ +export type BufferedKeyDefinitionOptions = + UserKeyDefinitionOptions & { + /** Checks whether the input type can be converted to the output type. + * @param input the data that is rolling over. + * @returns `true` if the definition is valid, otherwise `false`. If this + * function is not specified, any truthy input is valid. + * + * @remarks this is intended for cases where you're working with validated or + * signed data. It should be used to prevent data from being "laundered" through + * synchronized state. + */ + isValid?: (input: Input, dependency: Dependency) => Promise; + + /** Transforms the input data format to its output format. + * @param input the data that is rolling over. + * @returns the converted value. If this function is not specified, the value + * is asserted as the output type. + * + * @remarks This is intended for converting between, say, a replication format + * and a disk format or rotating encryption keys. + */ + map?: (input: Input, dependency: Dependency) => Promise; + + /** Checks whether an overwrite should occur + * @param dependency the latest value from the dependency observable provided + * to the buffered state. + * @returns `true` if a overwrite should occur, otherwise `false`. If this + * function is not specified, overwrites occur when the dependency is truthy. + * + * @remarks This is intended for waiting to overwrite until a dependency becomes + * available (e.g. an encryption key or a user confirmation). + */ + shouldOverwrite?: (dependency: Dependency) => boolean; + }; + +/** Storage and mapping settings for data stored by a `BufferedState`. + */ +export class BufferedKeyDefinition { + /** + * Defines a buffered state + * @param stateDefinition The domain of the buffer + * @param key Domain key that identifies the buffered value. This key must + * not be reused in any capacity. + * @param options Configures the operation of the buffered state. + */ + constructor( + readonly stateDefinition: StateDefinition, + readonly key: string, + readonly options: BufferedKeyDefinitionOptions, + ) {} + + /** Converts the buffered key definition to a state provider + * key definition + */ + toKeyDefinition() { + const bufferedKey = new UserKeyDefinition(this.stateDefinition, this.key, this.options); + + return bufferedKey; + } + + /** Checks whether the dependency triggers an overwrite. */ + shouldOverwrite(dependency: Dependency) { + const shouldOverwrite = this.options?.shouldOverwrite; + if (shouldOverwrite) { + return shouldOverwrite(dependency); + } + + return dependency ? true : false; + } + + /** Converts the input data format to its output format. + * @returns the converted value. + */ + map(input: Input, dependency: Dependency) { + const map = this.options?.map; + if (map) { + return map(input, dependency); + } + + return Promise.resolve(input as unknown as Output); + } + + /** Checks whether the input type can be converted to the output type. + * @returns `true` if the definition is defined and valid, otherwise `false`. + */ + isValid(input: Input, dependency: Dependency) { + if (input === null) { + return Promise.resolve(false); + } + + const isValid = this.options?.isValid; + if (isValid) { + return isValid(input, dependency); + } + + return Promise.resolve(input ? true : false); + } +} diff --git a/libs/common/src/tools/generator/state/buffered-state.spec.ts b/libs/common/src/tools/generator/state/buffered-state.spec.ts new file mode 100644 index 0000000000..46e132c1bd --- /dev/null +++ b/libs/common/src/tools/generator/state/buffered-state.spec.ts @@ -0,0 +1,381 @@ +import { BehaviorSubject, firstValueFrom, of } from "rxjs"; + +import { + mockAccountServiceWith, + FakeStateProvider, + awaitAsync, + trackEmissions, +} from "../../../../spec"; +import { GENERATOR_DISK, KeyDefinition } from "../../../platform/state"; +import { UserId } from "../../../types/guid"; + +import { BufferedKeyDefinition } from "./buffered-key-definition"; +import { BufferedState } from "./buffered-state"; + +const SomeUser = "SomeUser" as UserId; +const accountService = mockAccountServiceWith(SomeUser); +type SomeType = { foo: boolean; bar: boolean }; + +const SOME_KEY = new KeyDefinition(GENERATOR_DISK, "fooBar", { + deserializer: (jsonValue) => jsonValue as SomeType, +}); +const BUFFER_KEY = new BufferedKeyDefinition(GENERATOR_DISK, "fooBar_buffer", { + deserializer: (jsonValue) => jsonValue as SomeType, + clearOn: [], +}); + +describe("BufferedState", () => { + describe("state$", function () { + it("reads from the output state", async () => { + const provider = new FakeStateProvider(accountService); + const value = { foo: true, bar: false }; + const outputState = provider.getUser(SomeUser, SOME_KEY); + await outputState.update(() => value); + const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState); + + const result = await firstValueFrom(bufferedState.state$); + + expect(result).toEqual(value); + }); + + it("updates when the output state updates", async () => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + const secondValue = { foo: true, bar: true }; + await outputState.update(() => firstValue); + const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState); + + const result = trackEmissions(bufferedState.state$); + await outputState.update(() => secondValue); + await awaitAsync(); + + expect(result).toEqual([firstValue, secondValue]); + }); + + // this test is important for data migrations, which set + // the buffered state without using the `BufferedState` abstraction. + it.each([[null], [undefined]])( + "reads from the output state when the buffered state is '%p'", + async (bufferValue) => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + await outputState.update(() => firstValue); + const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState); + await provider.setUserState(BUFFER_KEY.toKeyDefinition(), bufferValue, SomeUser); + + const result = await firstValueFrom(bufferedState.state$); + + expect(result).toEqual(firstValue); + }, + ); + + // also important for data migrations + it("rolls over pending values from the buffered state immediately by default", async () => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const initialValue = { foo: true, bar: false }; + await outputState.update(() => initialValue); + const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState); + const bufferedValue = { foo: true, bar: true }; + await provider.setUserState(BUFFER_KEY.toKeyDefinition(), bufferedValue, SomeUser); + + const result = await trackEmissions(bufferedState.state$); + await awaitAsync(); + + expect(result).toEqual([initialValue, bufferedValue]); + }); + + // also important for data migrations + it("reads from the output state when its dependency is false", async () => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const value = { foo: true, bar: false }; + await outputState.update(() => value); + const dependency = new BehaviorSubject(false).asObservable(); + const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState, dependency); + await provider.setUserState(BUFFER_KEY.toKeyDefinition(), { foo: true, bar: true }, SomeUser); + + const result = await firstValueFrom(bufferedState.state$); + + expect(result).toEqual(value); + }); + + // also important for data migrations + it("overwrites the output state when its dependency emits a truthy value", async () => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + await outputState.update(() => firstValue); + const dependency = new BehaviorSubject(false); + const bufferedState = new BufferedState( + provider, + BUFFER_KEY, + outputState, + dependency.asObservable(), + ); + const bufferedValue = { foo: true, bar: true }; + await provider.setUserState(BUFFER_KEY.toKeyDefinition(), bufferedValue, SomeUser); + + const result = trackEmissions(bufferedState.state$); + dependency.next(true); + await awaitAsync(); + + expect(result).toEqual([firstValue, bufferedValue]); + }); + + it("overwrites the output state when shouldOverwrite returns a truthy value", async () => { + const bufferedKey = new BufferedKeyDefinition(GENERATOR_DISK, "fooBar_buffer", { + deserializer: (jsonValue) => jsonValue as SomeType, + shouldOverwrite: () => true, + clearOn: [], + }); + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const initialValue = { foo: true, bar: false }; + await outputState.update(() => initialValue); + const bufferedState = new BufferedState(provider, bufferedKey, outputState); + const bufferedValue = { foo: true, bar: true }; + await provider.setUserState(bufferedKey.toKeyDefinition(), bufferedValue, SomeUser); + + const result = await trackEmissions(bufferedState.state$); + await awaitAsync(); + + expect(result).toEqual([initialValue, bufferedValue]); + }); + + it("reads from the output state when shouldOverwrite returns a falsy value", async () => { + const bufferedKey = new BufferedKeyDefinition(GENERATOR_DISK, "fooBar_buffer", { + deserializer: (jsonValue) => jsonValue as SomeType, + shouldOverwrite: () => false, + clearOn: [], + }); + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const value = { foo: true, bar: false }; + await outputState.update(() => value); + const bufferedState = new BufferedState(provider, bufferedKey, outputState); + await provider.setUserState( + bufferedKey.toKeyDefinition(), + { foo: true, bar: true }, + SomeUser, + ); + + const result = await firstValueFrom(bufferedState.state$); + + expect(result).toEqual(value); + }); + + it("replaces the output state when shouldOverwrite transforms its dependency to a truthy value", async () => { + const bufferedKey = new BufferedKeyDefinition(GENERATOR_DISK, "fooBar_buffer", { + deserializer: (jsonValue) => jsonValue as SomeType, + shouldOverwrite: (dependency) => !dependency, + clearOn: [], + }); + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + await outputState.update(() => firstValue); + const dependency = new BehaviorSubject(true); + const bufferedState = new BufferedState( + provider, + bufferedKey, + outputState, + dependency.asObservable(), + ); + const bufferedValue = { foo: true, bar: true }; + await provider.setUserState(bufferedKey.toKeyDefinition(), bufferedValue, SomeUser); + + const result = trackEmissions(bufferedState.state$); + dependency.next(false); + await awaitAsync(); + + expect(result).toEqual([firstValue, bufferedValue]); + }); + }); + + describe("userId", () => { + const AnotherUser = "anotherUser" as UserId; + + it.each([[SomeUser], [AnotherUser]])("gets the userId", (userId) => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(userId, SOME_KEY); + const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState); + + const result = bufferedState.userId; + + expect(result).toEqual(userId); + }); + }); + + describe("update", () => { + it("updates state$", async () => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + const secondValue = { foo: true, bar: true }; + await outputState.update(() => firstValue); + const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState); + + const result = trackEmissions(bufferedState.state$); + await bufferedState.update(() => secondValue); + await awaitAsync(); + + expect(result).toEqual([firstValue, secondValue]); + }); + + it("respects update options", async () => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + const secondValue = { foo: true, bar: true }; + await outputState.update(() => firstValue); + const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState); + + const result = trackEmissions(bufferedState.state$); + await bufferedState.update(() => secondValue, { + shouldUpdate: (_, latest) => latest, + combineLatestWith: of(false), + }); + await awaitAsync(); + + expect(result).toEqual([firstValue]); + }); + }); + + describe("buffer", () => { + it("updates state$ once per overwrite", async () => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + const secondValue = { foo: true, bar: true }; + await outputState.update(() => firstValue); + const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState); + + const result = trackEmissions(bufferedState.state$); + await bufferedState.buffer(secondValue); + await awaitAsync(); + + expect(result).toEqual([firstValue, secondValue]); + }); + + it("emits the output state when its dependency is false", async () => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + await outputState.update(() => firstValue); + const dependency = new BehaviorSubject(false); + const bufferedState = new BufferedState( + provider, + BUFFER_KEY, + outputState, + dependency.asObservable(), + ); + const bufferedValue = { foo: true, bar: true }; + + const result = trackEmissions(bufferedState.state$); + await bufferedState.buffer(bufferedValue); + await awaitAsync(); + + expect(result).toEqual([firstValue]); + }); + + it("replaces the output state when its dependency becomes true", async () => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + await outputState.update(() => firstValue); + const dependency = new BehaviorSubject(false); + const bufferedState = new BufferedState( + provider, + BUFFER_KEY, + outputState, + dependency.asObservable(), + ); + const bufferedValue = { foo: true, bar: true }; + + const result = trackEmissions(bufferedState.state$); + await bufferedState.buffer(bufferedValue); + dependency.next(true); + await awaitAsync(); + + expect(result).toEqual([firstValue, bufferedValue]); + }); + + it.each([[null], [undefined]])("ignores `%p`", async (bufferedValue) => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + await outputState.update(() => firstValue); + const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState); + + const result = trackEmissions(bufferedState.state$); + await bufferedState.buffer(bufferedValue); + await awaitAsync(); + + expect(result).toEqual([firstValue]); + }); + + it("discards the buffered data when isValid returns false", async () => { + const bufferedKey = new BufferedKeyDefinition(GENERATOR_DISK, "fooBar_buffer", { + deserializer: (jsonValue) => jsonValue as SomeType, + isValid: () => Promise.resolve(false), + clearOn: [], + }); + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + await outputState.update(() => firstValue); + const bufferedState = new BufferedState(provider, bufferedKey, outputState); + + const stateResult = trackEmissions(bufferedState.state$); + await bufferedState.buffer({ foo: true, bar: true }); + await awaitAsync(); + const bufferedResult = await firstValueFrom(bufferedState.bufferedState$); + + expect(stateResult).toEqual([firstValue]); + expect(bufferedResult).toBeNull(); + }); + + it("overwrites the output when isValid returns true", async () => { + const bufferedKey = new BufferedKeyDefinition(GENERATOR_DISK, "fooBar_buffer", { + deserializer: (jsonValue) => jsonValue as SomeType, + isValid: () => Promise.resolve(true), + clearOn: [], + }); + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + await outputState.update(() => firstValue); + const bufferedState = new BufferedState(provider, bufferedKey, outputState); + const bufferedValue = { foo: true, bar: true }; + + const result = trackEmissions(bufferedState.state$); + await bufferedState.buffer(bufferedValue); + await awaitAsync(); + + expect(result).toEqual([firstValue, bufferedValue]); + }); + + it("maps the buffered data when it overwrites the state", async () => { + const mappedValue = { foo: true, bar: true }; + const bufferedKey = new BufferedKeyDefinition(GENERATOR_DISK, "fooBar_buffer", { + deserializer: (jsonValue) => jsonValue as SomeType, + map: () => Promise.resolve(mappedValue), + clearOn: [], + }); + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + await outputState.update(() => firstValue); + const bufferedState = new BufferedState(provider, bufferedKey, outputState); + + const result = trackEmissions(bufferedState.state$); + await bufferedState.buffer({ foo: false, bar: false }); + await awaitAsync(); + + expect(result).toEqual([firstValue, mappedValue]); + }); + }); +}); diff --git a/libs/common/src/tools/generator/state/buffered-state.ts b/libs/common/src/tools/generator/state/buffered-state.ts new file mode 100644 index 0000000000..bb4de645e9 --- /dev/null +++ b/libs/common/src/tools/generator/state/buffered-state.ts @@ -0,0 +1,129 @@ +import { Observable, combineLatest, concatMap, filter, map, of, concat, merge } from "rxjs"; + +import { + StateProvider, + SingleUserState, + CombinedState, + StateUpdateOptions, +} from "../../../platform/state"; + +import { BufferedKeyDefinition } from "./buffered-key-definition"; + +/** Stateful storage that overwrites one state with a buffered state. + * When a overwrite occurs, the input state is automatically deleted. + * @remarks The buffered state can only overwrite non-nullish values. If the + * buffer key contains `null` or `undefined`, it will do nothing. + */ +export class BufferedState implements SingleUserState { + /** + * Instantiate a buffered state + * @param provider constructs the buffer. + * @param key defines the buffer location. + * @param output updates when a overwrite occurs + * @param dependency$ provides data the buffer depends upon to evaluate and + * transform its data. If this is omitted, then `true` is injected as + * a dependency, which with a default output will trigger a overwrite immediately. + * + * @remarks `dependency$` enables overwrite control during dynamic circumstances, + * such as when a overwrite should occur only if a user key is available. + */ + constructor( + provider: StateProvider, + private key: BufferedKeyDefinition, + private output: SingleUserState, + dependency$: Observable = null, + ) { + this.bufferedState = provider.getUser(output.userId, key.toKeyDefinition()); + + // overwrite the output value + const hasValue$ = concat(of(null), this.bufferedState.state$).pipe( + map((buffer) => (buffer ?? null) !== null), + ); + const overwriteDependency$ = (dependency$ ?? of(true as unknown as Dependency)).pipe( + map((dependency) => [key.shouldOverwrite(dependency), dependency] as const), + ); + const overwrite$ = combineLatest([hasValue$, overwriteDependency$]).pipe( + concatMap(async ([hasValue, [shouldOverwrite, dependency]]) => { + if (hasValue && shouldOverwrite) { + await this.overwriteOutput(dependency); + } + return [false, null] as const; + }), + ); + + // drive overwrites only when there's a subscription; + // the output state determines when emissions occur + const output$ = this.output.state$.pipe(map((output) => [true, output] as const)); + this.state$ = merge(overwrite$, output$).pipe( + filter(([emit]) => emit), + map(([, output]) => output), + ); + + this.combinedState$ = this.state$.pipe(map((state) => [this.output.userId, state])); + + this.bufferedState$ = this.bufferedState.state$; + } + + private bufferedState: SingleUserState; + + private async overwriteOutput(dependency: Dependency) { + // take the latest value from the buffer + let buffered: Input; + await this.bufferedState.update((state) => { + buffered = state ?? null; + return null; + }); + + // update the output state + const isValid = await this.key.isValid(buffered, dependency); + if (isValid) { + const output = await this.key.map(buffered, dependency); + await this.output.update(() => output); + } + } + + /** {@link SingleUserState.userId} */ + get userId() { + return this.output.userId; + } + + /** Observes changes to the output state. This updates when the output + * state updates, when the buffer is moved to the output, and when `BufferedState.buffer` + * is invoked. + */ + readonly state$: Observable; + + /** {@link SingleUserState.combinedState$} */ + readonly combinedState$: Observable>; + + /** Buffers a value state. The buffered state overwrites the output + * state when a subscription occurs. + * @param value the state to roll over. Setting this to `null` or `undefined` + * has no effect. + */ + async buffer(value: Input): Promise { + const normalized = value ?? null; + if (normalized !== null) { + await this.bufferedState.update(() => normalized); + } + } + + /** The data presently being buffered. This emits the pending value each time + * new buffer data is provided. It emits null when the buffer is empty. + */ + readonly bufferedState$: Observable; + + /** Updates the output state. + * @param configureState a callback that returns an updated output + * state. The callback receives the state's present value as its + * first argument and the dependencies listed in `options.combinedLatestWith` + * as its second argument. + * @param options configures how the update is applied. See {@link StateUpdateOptions}. + */ + update( + configureState: (state: Output, dependencies: TCombine) => Output, + options: StateUpdateOptions = null, + ): Promise { + return this.output.update(configureState, options); + } +} diff --git a/libs/common/src/tools/generator/state/padded-data-packer.ts b/libs/common/src/tools/generator/state/padded-data-packer.ts index e2f5058b21..d1573e5cb7 100644 --- a/libs/common/src/tools/generator/state/padded-data-packer.ts +++ b/libs/common/src/tools/generator/state/padded-data-packer.ts @@ -58,11 +58,12 @@ export class PaddedDataPacker extends DataPackerAbstraction { /** {@link DataPackerAbstraction.unpack} */ unpack(secret: string): Jsonify { // frame size is stored before the JSON payload in base 10 - const frameBreakpoint = secret.indexOf(DATA_PACKING.divider); - if (frameBreakpoint < 1) { + const frameEndIndex = secret.indexOf(DATA_PACKING.divider); + if (frameEndIndex < 1) { throw new Error("missing frame size"); } - const frameSize = parseInt(secret.slice(0, frameBreakpoint), 10); + const frameSize = parseInt(secret.slice(0, frameEndIndex), 10); + const dataStartIndex = frameEndIndex + 1; // The decrypted string should be a multiple of the frame length if (secret.length % frameSize > 0) { @@ -70,20 +71,20 @@ export class PaddedDataPacker extends DataPackerAbstraction { } // encoded data terminates with the divider, followed by the padding character - const jsonBreakpoint = secret.lastIndexOf(DATA_PACKING.divider); - if (jsonBreakpoint == frameBreakpoint) { + const dataEndIndex = secret.lastIndexOf(DATA_PACKING.divider); + if (dataEndIndex == frameEndIndex) { throw new Error("missing json object"); } - const paddingBegins = jsonBreakpoint + 1; + const paddingStartIndex = dataEndIndex + 1; // If the padding contains invalid padding characters then the padding could be used // as a side channel for arbitrary data. - if (secret.slice(paddingBegins).match(DATA_PACKING.hasInvalidPadding)) { + if (secret.slice(paddingStartIndex).match(DATA_PACKING.hasInvalidPadding)) { throw new Error("invalid padding"); } // remove frame size and padding - const b64 = secret.substring(frameBreakpoint, paddingBegins); + const b64 = secret.slice(dataStartIndex, dataEndIndex); // unpack the stored data const json = Utils.fromB64ToUtf8(b64); diff --git a/libs/common/src/tools/generator/state/secret-key-definition.spec.ts b/libs/common/src/tools/generator/state/secret-key-definition.spec.ts index 7352631ff6..a347268b0b 100644 --- a/libs/common/src/tools/generator/state/secret-key-definition.spec.ts +++ b/libs/common/src/tools/generator/state/secret-key-definition.spec.ts @@ -1,16 +1,17 @@ -import { GENERATOR_DISK } from "../../../platform/state"; +import { GENERATOR_DISK, UserKeyDefinitionOptions } from "../../../platform/state"; import { SecretClassifier } from "./secret-classifier"; import { SecretKeyDefinition } from "./secret-key-definition"; describe("SecretKeyDefinition", () => { const classifier = SecretClassifier.allSecret<{ foo: boolean }>(); - const options = { deserializer: (v: any) => v }; + const options: UserKeyDefinitionOptions = { deserializer: (v: any) => v, clearOn: [] }; it("toEncryptedStateKey returns a key", () => { - const expectedOptions = { + const expectedOptions: UserKeyDefinitionOptions = { deserializer: (v: any) => v, cleanupDelayMs: 100, + clearOn: ["logout", "lock"], }; const definition = SecretKeyDefinition.value( GENERATOR_DISK, @@ -26,6 +27,7 @@ describe("SecretKeyDefinition", () => { expect(result.stateDefinition).toEqual(GENERATOR_DISK); expect(result.key).toBe("key"); expect(result.cleanupDelayMs).toBe(expectedOptions.cleanupDelayMs); + expect(result.clearOn).toEqual(expectedOptions.clearOn); expect(deserializerResult).toBe(expectedDeserializerResult); }); diff --git a/libs/common/src/tools/generator/state/secret-key-definition.ts b/libs/common/src/tools/generator/state/secret-key-definition.ts index 0de59be624..22496d878e 100644 --- a/libs/common/src/tools/generator/state/secret-key-definition.ts +++ b/libs/common/src/tools/generator/state/secret-key-definition.ts @@ -1,4 +1,4 @@ -import { KeyDefinition, KeyDefinitionOptions } from "../../../platform/state"; +import { UserKeyDefinitionOptions, UserKeyDefinition } from "../../../platform/state"; // eslint-disable-next-line -- `StateDefinition` used as an argument import { StateDefinition } from "../../../platform/state/state-definition"; import { ClassifiedFormat } from "./classified-format"; @@ -11,7 +11,7 @@ export class SecretKeyDefinition, - readonly options: KeyDefinitionOptions, + readonly options: UserKeyDefinitionOptions, // type erasure is necessary here because typescript doesn't support // higher kinded types that generalize over collections. The invariants // needed to make this typesafe are maintained by the static factories. @@ -21,12 +21,14 @@ export class SecretKeyDefinition[]>( + const secretKey = new UserKeyDefinition[]>( this.stateDefinition, this.key, { cleanupDelayMs: this.options.cleanupDelayMs, deserializer: (jsonValue) => jsonValue as ClassifiedFormat[], + // Clear encrypted state on logout + clearOn: this.options.clearOn, }, ); @@ -45,7 +47,7 @@ export class SecretKeyDefinition, - options: KeyDefinitionOptions, + options: UserKeyDefinitionOptions, ) { return new SecretKeyDefinition( stateDefinition, @@ -69,7 +71,7 @@ export class SecretKeyDefinition, - options: KeyDefinitionOptions, + options: UserKeyDefinitionOptions, ) { return new SecretKeyDefinition( stateDefinition, @@ -93,7 +95,7 @@ export class SecretKeyDefinition, - options: KeyDefinitionOptions, + options: UserKeyDefinitionOptions, ) { return new SecretKeyDefinition, Id, Item, Disclosed, Secret>( stateDefinition, diff --git a/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts b/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts index 086e347669..b4205b9fc9 100644 --- a/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts @@ -3,7 +3,7 @@ import { Observable, map, pipe } from "rxjs"; import { PolicyType } from "../../../admin-console/enums"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; -import { KeyDefinition, SingleUserState, StateProvider } from "../../../platform/state"; +import { SingleUserState, StateProvider, UserKeyDefinition } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { GeneratorStrategy } from "../abstractions"; import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; @@ -56,6 +56,7 @@ export abstract class ForwarderGeneratorStrategy< const key = SecretKeyDefinition.value(this.key.stateDefinition, this.key.key, classifier, { deserializer: (d) => this.key.deserializer(d), cleanupDelayMs: this.key.cleanupDelayMs, + clearOn: this.key.clearOn, }); // the type parameter is explicit because type inference fails for `Omit` @@ -83,7 +84,7 @@ export abstract class ForwarderGeneratorStrategy< abstract defaults$: (userId: UserId) => Observable; /** Determine where forwarder configuration is stored */ - protected abstract readonly key: KeyDefinition; + protected abstract readonly key: UserKeyDefinition; /** {@link GeneratorStrategy.toEvaluator} */ toEvaluator = () => { diff --git a/libs/common/src/tools/send/services/key-definitions.ts b/libs/common/src/tools/send/services/key-definitions.ts index b117c52268..f1a6b3d6c6 100644 --- a/libs/common/src/tools/send/services/key-definitions.ts +++ b/libs/common/src/tools/send/services/key-definitions.ts @@ -1,13 +1,23 @@ -import { KeyDefinition, SEND_DISK, SEND_MEMORY } from "../../../platform/state"; +import { SEND_DISK, SEND_MEMORY, UserKeyDefinition } from "../../../platform/state"; import { SendData } from "../models/data/send.data"; import { SendView } from "../models/view/send.view"; /** Encrypted send state stored on disk */ -export const SEND_USER_ENCRYPTED = KeyDefinition.record(SEND_DISK, "sendUserEncrypted", { - deserializer: (obj: SendData) => obj, -}); +export const SEND_USER_ENCRYPTED = UserKeyDefinition.record( + SEND_DISK, + "sendUserEncrypted", + { + deserializer: (obj: SendData) => obj, + clearOn: ["logout"], + }, +); /** Decrypted send state stored in memory */ -export const SEND_USER_DECRYPTED = new KeyDefinition(SEND_MEMORY, "sendUserDecrypted", { - deserializer: (obj) => obj, -}); +export const SEND_USER_DECRYPTED = new UserKeyDefinition( + SEND_MEMORY, + "sendUserDecrypted", + { + deserializer: (obj) => obj, + clearOn: ["lock"], + }, +); diff --git a/libs/common/src/tools/send/services/send.service.spec.ts b/libs/common/src/tools/send/services/send.service.spec.ts index fc793dba67..41183c42af 100644 --- a/libs/common/src/tools/send/services/send.service.spec.ts +++ b/libs/common/src/tools/send/services/send.service.spec.ts @@ -8,7 +8,6 @@ import { awaitAsync, mockAccountServiceWith, } from "../../../../spec"; -import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; @@ -64,7 +63,6 @@ describe("SendService", () => { id: mockUserId, email: "email", name: "name", - status: AuthenticationStatus.Unlocked, }); // Initial encrypted state diff --git a/libs/common/src/types/guid.ts b/libs/common/src/types/guid.ts index 714f5dffc3..97c87e684e 100644 --- a/libs/common/src/types/guid.ts +++ b/libs/common/src/types/guid.ts @@ -8,3 +8,4 @@ export type CollectionId = Opaque; export type ProviderId = Opaque; export type PolicyId = Opaque; export type CipherId = Opaque; +export type IndexedEntityId = Opaque; diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index a8a0a25e9b..22e2c54a59 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -1,3 +1,5 @@ +import { Observable } from "rxjs"; + import { UriMatchStrategySetting } from "../../models/domain/domain-service"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { CipherId, CollectionId, OrganizationId } from "../../types/guid"; @@ -7,8 +9,13 @@ import { Cipher } from "../models/domain/cipher"; import { Field } from "../models/domain/field"; import { CipherView } from "../models/view/cipher.view"; import { FieldView } from "../models/view/field.view"; +import { AddEditCipherInfo } from "../types/add-edit-cipher-info"; export abstract class CipherService { + /** + * An observable monitoring the add/edit cipher info saved to memory. + */ + addEditCipherInfo$: Observable; clearCache: (userId?: string) => Promise; encrypt: ( model: CipherView, @@ -40,8 +47,24 @@ export abstract class CipherService { updateLastUsedDate: (id: string) => Promise; updateLastLaunchedDate: (id: string) => Promise; saveNeverDomain: (domain: string) => Promise; - createWithServer: (cipher: Cipher, orgAdmin?: boolean) => Promise; - updateWithServer: (cipher: Cipher, orgAdmin?: boolean, isNotClone?: boolean) => Promise; + /** + * Create a cipher with the server + * + * @param cipher The cipher to create + * @param orgAdmin If true, the request is submitted as an organization admin request + * + * @returns A promise that resolves to the created cipher + */ + createWithServer: (cipher: Cipher, orgAdmin?: boolean) => Promise; + /** + * Update a cipher with the server + * @param cipher The cipher to update + * @param orgAdmin If true, the request is submitted as an organization admin request + * @param isNotClone If true, the cipher is not a clone and should be treated as a new cipher + * + * @returns A promise that resolves to the updated cipher + */ + updateWithServer: (cipher: Cipher, orgAdmin?: boolean, isNotClone?: boolean) => Promise; shareWithServer: ( cipher: CipherView, organizationId: string, @@ -63,7 +86,14 @@ export abstract class CipherService { data: ArrayBuffer, admin?: boolean, ) => Promise; - saveCollectionsWithServer: (cipher: Cipher) => Promise; + /** + * Save the collections for a cipher with the server + * + * @param cipher The cipher to save collections for + * + * @returns A promise that resolves when the collections have been saved + */ + saveCollectionsWithServer: (cipher: Cipher) => Promise; /** * Bulk update collections for many ciphers with the server * @param orgId @@ -77,7 +107,13 @@ export abstract class CipherService { collectionIds: CollectionId[], removeCollections: boolean, ) => Promise; - upsert: (cipher: CipherData | CipherData[]) => Promise; + /** + * Update the local store of CipherData with the provided data. Values are upserted into the existing store. + * + * @param cipher The cipher data to upsert. Can be a single CipherData object or an array of CipherData objects. + * @returns A promise that resolves to a record of updated cipher store, keyed by their cipher ID. Returns all ciphers, not just those updated + */ + upsert: (cipher: CipherData | CipherData[]) => Promise>; replace: (ciphers: { [id: string]: CipherData }) => Promise; clear: (userId: string) => Promise; moveManyWithServer: (ids: string[], folderId: string) => Promise; @@ -102,4 +138,5 @@ export abstract class CipherService { asAdmin?: boolean, ) => Promise; getKeyForCipherKeyDecryption: (cipher: Cipher) => Promise; + setAddEditCipherInfo: (value: AddEditCipherInfo) => Promise; } diff --git a/libs/common/src/vault/models/data/cipher.data.ts b/libs/common/src/vault/models/data/cipher.data.ts index 1452ffe7ee..f8db7186d6 100644 --- a/libs/common/src/vault/models/data/cipher.data.ts +++ b/libs/common/src/vault/models/data/cipher.data.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; import { CipherType } from "../../enums/cipher-type"; import { CipherResponse } from "../response/cipher.response"; @@ -84,4 +86,8 @@ export class CipherData { this.passwordHistory = response.passwordHistory.map((ph) => new PasswordHistoryData(ph)); } } + + static fromJSON(obj: Jsonify) { + return Object.assign(new CipherData(), obj); + } } diff --git a/libs/common/src/vault/models/response/collection.response.ts b/libs/common/src/vault/models/response/collection.response.ts index f16fe547e0..ac4781df71 100644 --- a/libs/common/src/vault/models/response/collection.response.ts +++ b/libs/common/src/vault/models/response/collection.response.ts @@ -21,6 +21,10 @@ export class CollectionDetailsResponse extends CollectionResponse { readOnly: boolean; manage: boolean; hidePasswords: boolean; + + /** + * Flag indicating the user has been explicitly assigned to this Collection + */ assigned: boolean; constructor(response: any) { @@ -35,15 +39,10 @@ export class CollectionDetailsResponse extends CollectionResponse { } } -export class CollectionAccessDetailsResponse extends CollectionResponse { +export class CollectionAccessDetailsResponse extends CollectionDetailsResponse { groups: SelectionReadOnlyResponse[] = []; users: SelectionReadOnlyResponse[] = []; - /** - * Flag indicating the user has been explicitly assigned to this Collection - */ - assigned: boolean; - constructor(response: any) { super(response); this.assigned = this.getResponseProperty("Assigned") || false; diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index c374724781..9b03753118 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -1,6 +1,8 @@ import { mock } from "jest-mock-extended"; import { of } from "rxjs"; +import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; +import { FakeStateProvider } from "../../../spec/fake-state-provider"; import { makeStaticByteArray } from "../../../spec/utils"; import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; @@ -12,10 +14,12 @@ import { CryptoService } from "../../platform/abstractions/crypto.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { StateService } from "../../platform/abstractions/state.service"; +import { Utils } from "../../platform/misc/utils"; import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer"; import { EncString } from "../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { ContainerService } from "../../platform/services/container.service"; +import { UserId } from "../../types/guid"; import { CipherKey, OrgKey } from "../../types/key"; import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service"; import { FieldType } from "../enums"; @@ -97,6 +101,8 @@ const cipherData: CipherData = { }, ], }; +const mockUserId = Utils.newGuid() as UserId; +let accountService: FakeAccountService; describe("Cipher Service", () => { const cryptoService = mock(); @@ -109,6 +115,8 @@ describe("Cipher Service", () => { const searchService = mock(); const encryptService = mock(); const configService = mock(); + accountService = mockAccountServiceWith(mockUserId); + const stateProvider = new FakeStateProvider(accountService); let cipherService: CipherService; let cipherObj: Cipher; @@ -130,6 +138,7 @@ describe("Cipher Service", () => { encryptService, cipherFileUploadService, configService, + stateProvider, ); cipherObj = new Cipher(cipherData); @@ -165,23 +174,20 @@ describe("Cipher Service", () => { it("should call apiService.postCipherAdmin when orgAdmin param is true and the cipher orgId != null", async () => { const spy = jest .spyOn(apiService, "postCipherAdmin") - .mockImplementation(() => Promise.resolve(cipherObj)); - // 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 - cipherService.createWithServer(cipherObj, true); + .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); + await cipherService.createWithServer(cipherObj, true); const expectedObj = new CipherCreateRequest(cipherObj); expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalledWith(expectedObj); }); + it("should call apiService.postCipher when orgAdmin param is true and the cipher orgId is null", async () => { cipherObj.organizationId = null; const spy = jest .spyOn(apiService, "postCipher") - .mockImplementation(() => Promise.resolve(cipherObj)); - // 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 - cipherService.createWithServer(cipherObj, true); + .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); + await cipherService.createWithServer(cipherObj, true); const expectedObj = new CipherRequest(cipherObj); expect(spy).toHaveBeenCalled(); @@ -192,10 +198,8 @@ describe("Cipher Service", () => { cipherObj.collectionIds = ["123"]; const spy = jest .spyOn(apiService, "postCipherCreate") - .mockImplementation(() => Promise.resolve(cipherObj)); - // 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 - cipherService.createWithServer(cipherObj); + .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); + await cipherService.createWithServer(cipherObj); const expectedObj = new CipherCreateRequest(cipherObj); expect(spy).toHaveBeenCalled(); @@ -205,10 +209,8 @@ describe("Cipher Service", () => { it("should call apiService.postCipher when orgAdmin and collectionIds logic is false", async () => { const spy = jest .spyOn(apiService, "postCipher") - .mockImplementation(() => Promise.resolve(cipherObj)); - // 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 - cipherService.createWithServer(cipherObj); + .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); + await cipherService.createWithServer(cipherObj); const expectedObj = new CipherRequest(cipherObj); expect(spy).toHaveBeenCalled(); @@ -220,10 +222,8 @@ describe("Cipher Service", () => { it("should call apiService.putCipherAdmin when orgAdmin and isNotClone params are true", async () => { const spy = jest .spyOn(apiService, "putCipherAdmin") - .mockImplementation(() => Promise.resolve(cipherObj)); - // 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 - cipherService.updateWithServer(cipherObj, true, true); + .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); + await cipherService.updateWithServer(cipherObj, true, true); const expectedObj = new CipherRequest(cipherObj); expect(spy).toHaveBeenCalled(); @@ -234,10 +234,8 @@ describe("Cipher Service", () => { cipherObj.edit = true; const spy = jest .spyOn(apiService, "putCipher") - .mockImplementation(() => Promise.resolve(cipherObj)); - // 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 - cipherService.updateWithServer(cipherObj); + .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); + await cipherService.updateWithServer(cipherObj); const expectedObj = new CipherRequest(cipherObj); expect(spy).toHaveBeenCalled(); @@ -248,10 +246,8 @@ describe("Cipher Service", () => { cipherObj.edit = false; const spy = jest .spyOn(apiService, "putPartialCipher") - .mockImplementation(() => Promise.resolve(cipherObj)); - // 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 - cipherService.updateWithServer(cipherObj); + .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); + await cipherService.updateWithServer(cipherObj); const expectedObj = new CipherPartialRequest(cipherObj); expect(spy).toHaveBeenCalled(); diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 4a6e96ead7..0b44636ea6 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -1,4 +1,4 @@ -import { firstValueFrom } from "rxjs"; +import { Observable, firstValueFrom, map, share, skipWhile, switchMap } from "rxjs"; import { SemVer } from "semver"; import { ApiService } from "../../abstractions/api.service"; @@ -21,13 +21,21 @@ import Domain from "../../platform/models/domain/domain-base"; import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer"; import { EncString } from "../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; +import { + ActiveUserState, + CIPHERS_MEMORY, + DeriveDefinition, + DerivedState, + StateProvider, +} from "../../platform/state"; import { CipherId, CollectionId, OrganizationId } from "../../types/guid"; -import { OrgKey, UserKey } from "../../types/key"; +import { UserKey, OrgKey } from "../../types/key"; import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service"; import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service"; import { FieldType } from "../enums"; import { CipherType } from "../enums/cipher-type"; import { CipherData } from "../models/data/cipher.data"; +import { LocalData } from "../models/data/local.data"; import { Attachment } from "../models/domain/attachment"; import { Card } from "../models/domain/card"; import { Cipher } from "../models/domain/cipher"; @@ -54,6 +62,14 @@ import { AttachmentView } from "../models/view/attachment.view"; import { CipherView } from "../models/view/cipher.view"; import { FieldView } from "../models/view/field.view"; import { PasswordHistoryView } from "../models/view/password-history.view"; +import { AddEditCipherInfo } from "../types/add-edit-cipher-info"; + +import { + ENCRYPTED_CIPHERS, + LOCAL_DATA_KEY, + ADD_EDIT_CIPHER_INFO_KEY, + DECRYPTED_CIPHERS, +} from "./key-state/ciphers.state"; const CIPHER_KEY_ENC_MIN_SERVER_VER = new SemVer("2024.2.0"); @@ -61,6 +77,20 @@ export class CipherService implements CipherServiceAbstraction { private sortedCiphersCache: SortedCiphersCache = new SortedCiphersCache( this.sortCiphersByLastUsed, ); + private ciphersExpectingUpdate: DerivedState; + + localData$: Observable>; + ciphers$: Observable>; + cipherViews$: Observable>; + viewFor$(id: CipherId) { + return this.cipherViews$.pipe(map((views) => views[id])); + } + addEditCipherInfo$: Observable; + + private localDataState: ActiveUserState>; + private encryptedCiphersState: ActiveUserState>; + private decryptedCiphersState: ActiveUserState>; + private addEditCipherInfoState: ActiveUserState; constructor( private cryptoService: CryptoService, @@ -73,11 +103,36 @@ export class CipherService implements CipherServiceAbstraction { private encryptService: EncryptService, private cipherFileUploadService: CipherFileUploadService, private configService: ConfigService, - ) {} + private stateProvider: StateProvider, + ) { + this.localDataState = this.stateProvider.getActive(LOCAL_DATA_KEY); + this.encryptedCiphersState = this.stateProvider.getActive(ENCRYPTED_CIPHERS); + this.decryptedCiphersState = this.stateProvider.getActive(DECRYPTED_CIPHERS); + this.addEditCipherInfoState = this.stateProvider.getActive(ADD_EDIT_CIPHER_INFO_KEY); + this.ciphersExpectingUpdate = this.stateProvider.getDerived( + this.encryptedCiphersState.state$, + new DeriveDefinition(CIPHERS_MEMORY, "ciphersExpectingUpdate", { + derive: (_: Record) => false, + deserializer: (value) => value, + }), + {}, + ); - async getDecryptedCipherCache(): Promise { - const decryptedCiphers = await this.stateService.getDecryptedCiphers(); - return decryptedCiphers; + this.localData$ = this.localDataState.state$.pipe(map((data) => data ?? {})); + // First wait for ciphersExpectingUpdate to be false before emitting ciphers + this.ciphers$ = this.ciphersExpectingUpdate.state$.pipe( + skipWhile((expectingUpdate) => expectingUpdate), + switchMap(() => this.encryptedCiphersState.state$), + map((ciphers) => ciphers ?? {}), + ); + this.cipherViews$ = this.decryptedCiphersState.state$.pipe( + map((views) => views ?? {}), + + share({ + resetOnRefCountZero: true, + }), + ); + this.addEditCipherInfo$ = this.addEditCipherInfoState.state$; } async setDecryptedCipherCache(value: CipherView[]) { @@ -85,17 +140,25 @@ export class CipherService implements CipherServiceAbstraction { // if we cache it then we may accidentially return it when it's not right, we'd rather try decryption again. // We still want to set null though, that is the indicator that the cache isn't valid and we should do decryption. if (value == null || value.length !== 0) { - await this.stateService.setDecryptedCiphers(value); + await this.setDecryptedCiphers(value); } if (this.searchService != null) { if (value == null) { - this.searchService.clearIndex(); + await this.searchService.clearIndex(); } else { - this.searchService.indexCiphers(value); + await this.searchService.indexCiphers(value); } } } + private async setDecryptedCiphers(value: CipherView[]) { + const cipherViews: { [id: string]: CipherView } = {}; + value?.forEach((c) => { + cipherViews[c.id] = c; + }); + await this.decryptedCiphersState.update(() => cipherViews); + } + async clearCache(userId?: string): Promise { await this.clearDecryptedCiphersState(userId); } @@ -268,24 +331,27 @@ export class CipherService implements CipherServiceAbstraction { } async get(id: string): Promise { - const ciphers = await this.stateService.getEncryptedCiphers(); + const ciphers = await firstValueFrom(this.ciphers$); // eslint-disable-next-line if (ciphers == null || !ciphers.hasOwnProperty(id)) { return null; } - const localData = await this.stateService.getLocalData(); - return new Cipher(ciphers[id], localData ? localData[id] : null); + const localData = await firstValueFrom(this.localData$); + const cipherId = id as CipherId; + + return new Cipher(ciphers[cipherId], localData ? localData[cipherId] : null); } async getAll(): Promise { - const localData = await this.stateService.getLocalData(); - const ciphers = await this.stateService.getEncryptedCiphers(); + const localData = await firstValueFrom(this.localData$); + const ciphers = await firstValueFrom(this.ciphers$); const response: Cipher[] = []; for (const id in ciphers) { // eslint-disable-next-line if (ciphers.hasOwnProperty(id)) { - response.push(new Cipher(ciphers[id], localData ? localData[id] : null)); + const cipherId = id as CipherId; + response.push(new Cipher(ciphers[cipherId], localData ? localData[cipherId] : null)); } } return response; @@ -293,12 +359,23 @@ export class CipherService implements CipherServiceAbstraction { @sequentialize(() => "getAllDecrypted") async getAllDecrypted(): Promise { - if ((await this.getDecryptedCipherCache()) != null) { + let decCiphers = await this.getDecryptedCiphers(); + if (decCiphers != null && decCiphers.length !== 0) { await this.reindexCiphers(); - return await this.getDecryptedCipherCache(); + return await this.getDecryptedCiphers(); } - const ciphers = await this.getAll(); + decCiphers = await this.decryptCiphers(await this.getAll()); + + await this.setDecryptedCipherCache(decCiphers); + return decCiphers; + } + + private async getDecryptedCiphers() { + return Object.values(await firstValueFrom(this.cipherViews$)); + } + + private async decryptCiphers(ciphers: Cipher[]) { const orgKeys = await this.cryptoService.getOrgKeys(); const userKey = await this.cryptoService.getUserKeyWithLegacySupport(); if (Object.keys(orgKeys).length === 0 && userKey == null) { @@ -326,16 +403,16 @@ export class CipherService implements CipherServiceAbstraction { .flat() .sort(this.getLocaleSortingFunction()); - await this.setDecryptedCipherCache(decCiphers); return decCiphers; } private async reindexCiphers() { const userId = await this.stateService.getUserId(); const reindexRequired = - this.searchService != null && (this.searchService.indexedEntityId ?? userId) !== userId; + this.searchService != null && + ((await firstValueFrom(this.searchService.indexedEntityId$)) ?? userId) !== userId; if (reindexRequired) { - this.searchService.indexCiphers(await this.getDecryptedCipherCache(), userId); + await this.searchService.indexCiphers(await this.getDecryptedCiphers(), userId); } } @@ -447,22 +524,24 @@ export class CipherService implements CipherServiceAbstraction { } async updateLastUsedDate(id: string): Promise { - let ciphersLocalData = await this.stateService.getLocalData(); + let ciphersLocalData = await firstValueFrom(this.localData$); + if (!ciphersLocalData) { ciphersLocalData = {}; } - if (ciphersLocalData[id]) { - ciphersLocalData[id].lastUsedDate = new Date().getTime(); + const cipherId = id as CipherId; + if (ciphersLocalData[cipherId]) { + ciphersLocalData[cipherId].lastUsedDate = new Date().getTime(); } else { - ciphersLocalData[id] = { + ciphersLocalData[cipherId] = { lastUsedDate: new Date().getTime(), }; } - await this.stateService.setLocalData(ciphersLocalData); + await this.localDataState.update(() => ciphersLocalData); - const decryptedCipherCache = await this.stateService.getDecryptedCiphers(); + const decryptedCipherCache = await this.getDecryptedCiphers(); if (!decryptedCipherCache) { return; } @@ -470,30 +549,32 @@ export class CipherService implements CipherServiceAbstraction { for (let i = 0; i < decryptedCipherCache.length; i++) { const cached = decryptedCipherCache[i]; if (cached.id === id) { - cached.localData = ciphersLocalData[id]; + cached.localData = ciphersLocalData[id as CipherId]; break; } } - await this.stateService.setDecryptedCiphers(decryptedCipherCache); + await this.setDecryptedCiphers(decryptedCipherCache); } async updateLastLaunchedDate(id: string): Promise { - let ciphersLocalData = await this.stateService.getLocalData(); + let ciphersLocalData = await firstValueFrom(this.localData$); + if (!ciphersLocalData) { ciphersLocalData = {}; } - if (ciphersLocalData[id]) { - ciphersLocalData[id].lastLaunched = new Date().getTime(); + const cipherId = id as CipherId; + if (ciphersLocalData[cipherId]) { + ciphersLocalData[cipherId].lastLaunched = new Date().getTime(); } else { - ciphersLocalData[id] = { + ciphersLocalData[cipherId] = { lastUsedDate: new Date().getTime(), }; } - await this.stateService.setLocalData(ciphersLocalData); + await this.localDataState.update(() => ciphersLocalData); - const decryptedCipherCache = await this.stateService.getDecryptedCiphers(); + const decryptedCipherCache = await this.getDecryptedCiphers(); if (!decryptedCipherCache) { return; } @@ -501,11 +582,11 @@ export class CipherService implements CipherServiceAbstraction { for (let i = 0; i < decryptedCipherCache.length; i++) { const cached = decryptedCipherCache[i]; if (cached.id === id) { - cached.localData = ciphersLocalData[id]; + cached.localData = ciphersLocalData[id as CipherId]; break; } } - await this.stateService.setDecryptedCiphers(decryptedCipherCache); + await this.setDecryptedCiphers(decryptedCipherCache); } async saveNeverDomain(domain: string): Promise { @@ -521,7 +602,7 @@ export class CipherService implements CipherServiceAbstraction { await this.domainSettingsService.setNeverDomains(domains); } - async createWithServer(cipher: Cipher, orgAdmin?: boolean): Promise { + async createWithServer(cipher: Cipher, orgAdmin?: boolean): Promise { let response: CipherResponse; if (orgAdmin && cipher.organizationId != null) { const request = new CipherCreateRequest(cipher); @@ -536,10 +617,16 @@ export class CipherService implements CipherServiceAbstraction { cipher.id = response.id; const data = new CipherData(response, cipher.collectionIds); - await this.upsert(data); + const updated = await this.upsert(data); + // No local data for new ciphers + return new Cipher(updated[cipher.id as CipherId]); } - async updateWithServer(cipher: Cipher, orgAdmin?: boolean, isNotClone?: boolean): Promise { + async updateWithServer( + cipher: Cipher, + orgAdmin?: boolean, + isNotClone?: boolean, + ): Promise { let response: CipherResponse; if (orgAdmin && isNotClone) { const request = new CipherRequest(cipher); @@ -553,7 +640,9 @@ export class CipherService implements CipherServiceAbstraction { } const data = new CipherData(response, cipher.collectionIds); - await this.upsert(data); + const updated = await this.upsert(data); + // updating with server does not change local data + return new Cipher(updated[cipher.id as CipherId], cipher.localData); } async shareWithServer( @@ -680,11 +769,13 @@ export class CipherService implements CipherServiceAbstraction { return new Cipher(cData); } - async saveCollectionsWithServer(cipher: Cipher): Promise { + async saveCollectionsWithServer(cipher: Cipher): Promise { const request = new CipherCollectionsRequest(cipher.collectionIds); - await this.apiService.putCipherCollections(cipher.id, request); - const data = cipher.toCipherData(); - await this.upsert(data); + const response = await this.apiService.putCipherCollections(cipher.id, request); + const data = new CipherData(response); + const updated = await this.upsert(data); + // Collection updates don't change local data + return new Cipher(updated[cipher.id as CipherId], cipher.localData); } /** @@ -710,7 +801,7 @@ export class CipherService implements CipherServiceAbstraction { await this.apiService.send("POST", "/ciphers/bulk-collections", request, true, false); // Update the local state - const ciphers = await this.stateService.getEncryptedCiphers(); + const ciphers = await firstValueFrom(this.ciphers$); for (const id of cipherIds) { const cipher = ciphers[id]; @@ -727,30 +818,32 @@ export class CipherService implements CipherServiceAbstraction { } await this.clearCache(); - await this.stateService.setEncryptedCiphers(ciphers); + await this.encryptedCiphersState.update(() => ciphers); } - async upsert(cipher: CipherData | CipherData[]): Promise { - let ciphers = await this.stateService.getEncryptedCiphers(); - if (ciphers == null) { - ciphers = {}; - } - - if (cipher instanceof CipherData) { - const c = cipher as CipherData; - ciphers[c.id] = c; - } else { - (cipher as CipherData[]).forEach((c) => { - ciphers[c.id] = c; - }); - } - - await this.replace(ciphers); + async upsert(cipher: CipherData | CipherData[]): Promise> { + const ciphers = cipher instanceof CipherData ? [cipher] : cipher; + return await this.updateEncryptedCipherState((current) => { + ciphers.forEach((c) => (current[c.id as CipherId] = c)); + return current; + }); } async replace(ciphers: { [id: string]: CipherData }): Promise { + await this.updateEncryptedCipherState(() => ciphers); + } + + private async updateEncryptedCipherState( + update: (current: Record) => Record, + ): Promise> { + // Store that we should wait for an update to return any ciphers + await this.ciphersExpectingUpdate.forceValue(true); await this.clearDecryptedCiphersState(); - await this.stateService.setEncryptedCiphers(ciphers); + const [, updatedCiphers] = await this.encryptedCiphersState.update((current) => { + const result = update(current ?? {}); + return result; + }); + return updatedCiphers; } async clear(userId?: string): Promise { @@ -761,7 +854,7 @@ export class CipherService implements CipherServiceAbstraction { async moveManyWithServer(ids: string[], folderId: string): Promise { await this.apiService.putMoveCiphers(new CipherBulkMoveRequest(ids, folderId)); - let ciphers = await this.stateService.getEncryptedCiphers(); + let ciphers = await firstValueFrom(this.ciphers$); if (ciphers == null) { ciphers = {}; } @@ -769,33 +862,34 @@ export class CipherService implements CipherServiceAbstraction { ids.forEach((id) => { // eslint-disable-next-line if (ciphers.hasOwnProperty(id)) { - ciphers[id].folderId = folderId; + ciphers[id as CipherId].folderId = folderId; } }); await this.clearCache(); - await this.stateService.setEncryptedCiphers(ciphers); + await this.encryptedCiphersState.update(() => ciphers); } async delete(id: string | string[]): Promise { - const ciphers = await this.stateService.getEncryptedCiphers(); + const ciphers = await firstValueFrom(this.ciphers$); if (ciphers == null) { return; } if (typeof id === "string") { - if (ciphers[id] == null) { + const cipherId = id as CipherId; + if (ciphers[cipherId] == null) { return; } - delete ciphers[id]; + delete ciphers[cipherId]; } else { - (id as string[]).forEach((i) => { + (id as CipherId[]).forEach((i) => { delete ciphers[i]; }); } await this.clearCache(); - await this.stateService.setEncryptedCiphers(ciphers); + await this.encryptedCiphersState.update(() => ciphers); } async deleteWithServer(id: string, asAdmin = false): Promise { @@ -819,21 +913,26 @@ export class CipherService implements CipherServiceAbstraction { } async deleteAttachment(id: string, attachmentId: string): Promise { - const ciphers = await this.stateService.getEncryptedCiphers(); - + let ciphers = await firstValueFrom(this.ciphers$); + const cipherId = id as CipherId; // eslint-disable-next-line - if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[id].attachments == null) { + if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[cipherId].attachments == null) { return; } - for (let i = 0; i < ciphers[id].attachments.length; i++) { - if (ciphers[id].attachments[i].id === attachmentId) { - ciphers[id].attachments.splice(i, 1); + for (let i = 0; i < ciphers[cipherId].attachments.length; i++) { + if (ciphers[cipherId].attachments[i].id === attachmentId) { + ciphers[cipherId].attachments.splice(i, 1); } } await this.clearCache(); - await this.stateService.setEncryptedCiphers(ciphers); + await this.encryptedCiphersState.update(() => { + if (ciphers == null) { + ciphers = {}; + } + return ciphers; + }); } async deleteAttachmentWithServer(id: string, attachmentId: string): Promise { @@ -916,12 +1015,12 @@ export class CipherService implements CipherServiceAbstraction { } async softDelete(id: string | string[]): Promise { - const ciphers = await this.stateService.getEncryptedCiphers(); + let ciphers = await firstValueFrom(this.ciphers$); if (ciphers == null) { return; } - const setDeletedDate = (cipherId: string) => { + const setDeletedDate = (cipherId: CipherId) => { if (ciphers[cipherId] == null) { return; } @@ -929,13 +1028,18 @@ export class CipherService implements CipherServiceAbstraction { }; if (typeof id === "string") { - setDeletedDate(id); + setDeletedDate(id as CipherId); } else { (id as string[]).forEach(setDeletedDate); } await this.clearCache(); - await this.stateService.setEncryptedCiphers(ciphers); + await this.encryptedCiphersState.update(() => { + if (ciphers == null) { + ciphers = {}; + } + return ciphers; + }); } async softDeleteWithServer(id: string, asAdmin = false): Promise { @@ -962,17 +1066,18 @@ export class CipherService implements CipherServiceAbstraction { async restore( cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[], ) { - const ciphers = await this.stateService.getEncryptedCiphers(); + let ciphers = await firstValueFrom(this.ciphers$); if (ciphers == null) { return; } const clearDeletedDate = (c: { id: string; revisionDate: string }) => { - if (ciphers[c.id] == null) { + const cipherId = c.id as CipherId; + if (ciphers[cipherId] == null) { return; } - ciphers[c.id].deletedDate = null; - ciphers[c.id].revisionDate = c.revisionDate; + ciphers[cipherId].deletedDate = null; + ciphers[cipherId].revisionDate = c.revisionDate; }; if (cipher.constructor.name === Array.name) { @@ -982,7 +1087,12 @@ export class CipherService implements CipherServiceAbstraction { } await this.clearCache(); - await this.stateService.setEncryptedCiphers(ciphers); + await this.encryptedCiphersState.update(() => { + if (ciphers == null) { + ciphers = {}; + } + return ciphers; + }); } async restoreWithServer(id: string, asAdmin = false): Promise { @@ -1024,6 +1134,10 @@ export class CipherService implements CipherServiceAbstraction { ); } + async setAddEditCipherInfo(value: AddEditCipherInfo) { + await this.addEditCipherInfoState.update(() => value); + } + // Helpers // In the case of a cipher that is being shared with an organization, we want to decrypt the @@ -1349,11 +1463,11 @@ export class CipherService implements CipherServiceAbstraction { } private async clearEncryptedCiphersState(userId?: string) { - await this.stateService.setEncryptedCiphers(null, { userId: userId }); + await this.encryptedCiphersState.update(() => ({})); } private async clearDecryptedCiphersState(userId?: string) { - await this.stateService.setDecryptedCiphers(null, { userId: userId }); + await this.setDecryptedCiphers(null); this.clearSortedCiphers(); } diff --git a/libs/common/src/vault/services/fido2/fido2-client.service.ts b/libs/common/src/vault/services/fido2/fido2-client.service.ts index bfc8cbe915..4e0aab017a 100644 --- a/libs/common/src/vault/services/fido2/fido2-client.service.ts +++ b/libs/common/src/vault/services/fido2/fido2-client.service.ts @@ -48,20 +48,26 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { ) {} async isFido2FeatureEnabled(hostname: string, origin: string): Promise { - const userEnabledPasskeys = await firstValueFrom(this.vaultSettingsService.enablePasskeys$); const isUserLoggedIn = (await this.authService.getAuthStatus()) !== AuthenticationStatus.LoggedOut; + if (!isUserLoggedIn) { + return false; + } const neverDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); const isExcludedDomain = neverDomains != null && hostname in neverDomains; + if (isExcludedDomain) { + return false; + } const serverConfig = await firstValueFrom(this.configService.serverConfig$); const isOriginEqualBitwardenVault = origin === serverConfig.environment?.vault; + if (isOriginEqualBitwardenVault) { + return false; + } - return ( - userEnabledPasskeys && isUserLoggedIn && !isExcludedDomain && !isOriginEqualBitwardenVault - ); + return await firstValueFrom(this.vaultSettingsService.enablePasskeys$); } async createCredential( @@ -70,6 +76,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { abortController = new AbortController(), ): Promise { const parsedOrigin = parse(params.origin, { allowPrivateDomains: true }); + const enableFido2VaultCredentials = await this.isFido2FeatureEnabled( parsedOrigin.hostname, params.origin, @@ -346,7 +353,7 @@ function setAbortTimeout( ); } - return window.setTimeout(() => abortController.abort(), clampedTimeout); + return self.setTimeout(() => abortController.abort(), clampedTimeout); } /** diff --git a/libs/common/src/vault/services/fido2/fido2-utils.spec.ts b/libs/common/src/vault/services/fido2/fido2-utils.spec.ts new file mode 100644 index 0000000000..a05eab5230 --- /dev/null +++ b/libs/common/src/vault/services/fido2/fido2-utils.spec.ts @@ -0,0 +1,40 @@ +import { Fido2Utils } from "./fido2-utils"; + +describe("Fido2 Utils", () => { + const asciiHelloWorldArray = [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]; + const b64HelloWorldString = "aGVsbG8gd29ybGQ="; + + describe("fromBufferToB64(...)", () => { + it("should convert an ArrayBuffer to a b64 string", () => { + const buffer = new Uint8Array(asciiHelloWorldArray).buffer; + const b64String = Fido2Utils.fromBufferToB64(buffer); + expect(b64String).toBe(b64HelloWorldString); + }); + + it("should return an empty string when given an empty ArrayBuffer", () => { + const buffer = new Uint8Array([]).buffer; + const b64String = Fido2Utils.fromBufferToB64(buffer); + expect(b64String).toBe(""); + }); + + it("should return null when given null input", () => { + const b64String = Fido2Utils.fromBufferToB64(null); + expect(b64String).toBeNull(); + }); + }); + + describe("fromB64ToArray(...)", () => { + it("should convert a b64 string to an Uint8Array", () => { + const expectedArray = new Uint8Array(asciiHelloWorldArray); + + const resultArray = Fido2Utils.fromB64ToArray(b64HelloWorldString); + + expect(resultArray).toEqual(expectedArray); + }); + + it("should return null when given null input", () => { + const expectedArray = Fido2Utils.fromB64ToArray(null); + expect(expectedArray).toBeNull(); + }); + }); +}); diff --git a/libs/common/src/vault/services/fido2/fido2-utils.ts b/libs/common/src/vault/services/fido2/fido2-utils.ts index a2de137550..13c9762135 100644 --- a/libs/common/src/vault/services/fido2/fido2-utils.ts +++ b/libs/common/src/vault/services/fido2/fido2-utils.ts @@ -1,14 +1,20 @@ -import { Utils } from "../../../platform/misc/utils"; - export class Fido2Utils { static bufferToString(bufferSource: BufferSource): string { - const buffer = Fido2Utils.bufferSourceToUint8Array(bufferSource); + let buffer: Uint8Array; + if (bufferSource instanceof ArrayBuffer || bufferSource.buffer === undefined) { + buffer = new Uint8Array(bufferSource as ArrayBuffer); + } else { + buffer = new Uint8Array(bufferSource.buffer); + } - return Utils.fromBufferToUrlB64(buffer); + return Fido2Utils.fromBufferToB64(buffer) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); } static stringToBuffer(str: string): Uint8Array { - return Utils.fromUrlB64ToArray(str); + return Fido2Utils.fromB64ToArray(Fido2Utils.fromUrlB64ToB64(str)); } static bufferSourceToUint8Array(bufferSource: BufferSource) { @@ -23,4 +29,52 @@ export class Fido2Utils { private static isArrayBuffer(bufferSource: BufferSource): bufferSource is ArrayBuffer { return bufferSource instanceof ArrayBuffer || bufferSource.buffer === undefined; } + + static fromB64toUrlB64(b64Str: string) { + return b64Str.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); + } + + static fromBufferToB64(buffer: ArrayBuffer): string { + if (buffer == null) { + return null; + } + + let binary = ""; + const bytes = new Uint8Array(buffer); + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + return globalThis.btoa(binary); + } + + static fromB64ToArray(str: string): Uint8Array { + if (str == null) { + return null; + } + + const binaryString = globalThis.atob(str); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; + } + + static fromUrlB64ToB64(urlB64Str: string): string { + let output = urlB64Str.replace(/-/g, "+").replace(/_/g, "/"); + switch (output.length % 4) { + case 0: + break; + case 2: + output += "=="; + break; + case 3: + output += "="; + break; + default: + throw new Error("Illegal base64url string!"); + } + + return output; + } } diff --git a/libs/common/src/vault/services/folder/folder.service.spec.ts b/libs/common/src/vault/services/folder/folder.service.spec.ts index 88595720e2..8c3be9abe8 100644 --- a/libs/common/src/vault/services/folder/folder.service.spec.ts +++ b/libs/common/src/vault/services/folder/folder.service.spec.ts @@ -8,7 +8,6 @@ import { FakeStateProvider } from "../../../../spec/fake-state-provider"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; -import { StateService } from "../../../platform/abstractions/state.service"; import { Utils } from "../../../platform/misc/utils"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -27,7 +26,6 @@ describe("Folder Service", () => { let encryptService: MockProxy; let i18nService: MockProxy; let cipherService: MockProxy; - let stateService: MockProxy; let stateProvider: FakeStateProvider; const mockUserId = Utils.newGuid() as UserId; @@ -39,7 +37,6 @@ describe("Folder Service", () => { encryptService = mock(); i18nService = mock(); cipherService = mock(); - stateService = mock(); accountService = mockAccountServiceWith(mockUserId); stateProvider = new FakeStateProvider(accountService); @@ -52,13 +49,7 @@ describe("Folder Service", () => { ); encryptService.decryptToUtf8.mockResolvedValue("DEC"); - folderService = new FolderService( - cryptoService, - i18nService, - cipherService, - stateService, - stateProvider, - ); + folderService = new FolderService(cryptoService, i18nService, cipherService, stateProvider); folderState = stateProvider.activeUser.getFake(FOLDER_ENCRYPTED_FOLDERS); diff --git a/libs/common/src/vault/services/folder/folder.service.ts b/libs/common/src/vault/services/folder/folder.service.ts index afe3b01c68..584567aee8 100644 --- a/libs/common/src/vault/services/folder/folder.service.ts +++ b/libs/common/src/vault/services/folder/folder.service.ts @@ -2,17 +2,16 @@ import { Observable, firstValueFrom, map } from "rxjs"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; -import { StateService } from "../../../platform/abstractions/state.service"; import { Utils } from "../../../platform/misc/utils"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { ActiveUserState, DerivedState, StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { CipherService } from "../../../vault/abstractions/cipher.service"; import { InternalFolderService as InternalFolderServiceAbstraction } from "../../../vault/abstractions/folder/folder.service.abstraction"; -import { CipherData } from "../../../vault/models/data/cipher.data"; import { FolderData } from "../../../vault/models/data/folder.data"; import { Folder } from "../../../vault/models/domain/folder"; import { FolderView } from "../../../vault/models/view/folder.view"; +import { Cipher } from "../../models/domain/cipher"; import { FOLDER_DECRYPTED_FOLDERS, FOLDER_ENCRYPTED_FOLDERS } from "../key-state/folder.state"; export class FolderService implements InternalFolderServiceAbstraction { @@ -26,7 +25,6 @@ export class FolderService implements InternalFolderServiceAbstraction { private cryptoService: CryptoService, private i18nService: I18nService, private cipherService: CipherService, - private stateService: StateService, private stateProvider: StateProvider, ) { this.encryptedFoldersState = this.stateProvider.getActive(FOLDER_ENCRYPTED_FOLDERS); @@ -144,9 +142,9 @@ export class FolderService implements InternalFolderServiceAbstraction { }); // Items in a deleted folder are re-assigned to "No Folder" - const ciphers = await this.stateService.getEncryptedCiphers(); + const ciphers = await this.cipherService.getAll(); if (ciphers != null) { - const updates: CipherData[] = []; + const updates: Cipher[] = []; for (const cId in ciphers) { if (ciphers[cId].folderId === id) { ciphers[cId].folderId = null; @@ -156,7 +154,7 @@ export class FolderService implements InternalFolderServiceAbstraction { if (updates.length > 0) { // 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.upsert(updates); + this.cipherService.upsert(updates.map((c) => c.toCipherData())); } } } diff --git a/libs/common/src/vault/services/key-state/ciphers.state.ts b/libs/common/src/vault/services/key-state/ciphers.state.ts new file mode 100644 index 0000000000..71da4c2333 --- /dev/null +++ b/libs/common/src/vault/services/key-state/ciphers.state.ts @@ -0,0 +1,52 @@ +import { Jsonify } from "type-fest"; + +import { + CIPHERS_DISK, + CIPHERS_DISK_LOCAL, + CIPHERS_MEMORY, + KeyDefinition, +} from "../../../platform/state"; +import { CipherId } from "../../../types/guid"; +import { CipherData } from "../../models/data/cipher.data"; +import { LocalData } from "../../models/data/local.data"; +import { CipherView } from "../../models/view/cipher.view"; +import { AddEditCipherInfo } from "../../types/add-edit-cipher-info"; + +export const ENCRYPTED_CIPHERS = KeyDefinition.record(CIPHERS_DISK, "ciphers", { + deserializer: (obj: Jsonify) => CipherData.fromJSON(obj), +}); + +export const DECRYPTED_CIPHERS = KeyDefinition.record( + CIPHERS_MEMORY, + "decryptedCiphers", + { + deserializer: (cipher: Jsonify) => CipherView.fromJSON(cipher), + }, +); + +export const LOCAL_DATA_KEY = new KeyDefinition>( + CIPHERS_DISK_LOCAL, + "localData", + { + deserializer: (localData) => localData, + }, +); + +export const ADD_EDIT_CIPHER_INFO_KEY = new KeyDefinition( + CIPHERS_MEMORY, + "addEditCipherInfo", + { + deserializer: (addEditCipherInfo: AddEditCipherInfo) => { + if (addEditCipherInfo == null) { + return null; + } + + const cipher = + addEditCipherInfo?.cipher.toJSON != null + ? addEditCipherInfo.cipher + : CipherView.fromJSON(addEditCipherInfo?.cipher as Jsonify); + + return { cipher, collectionIds: addEditCipherInfo.collectionIds }; + }, + }, +); diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index d4601d9621..73869ff488 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -11,8 +11,11 @@ import { OrganizationData } from "../../../admin-console/models/data/organizatio import { PolicyData } from "../../../admin-console/models/data/policy.data"; import { ProviderData } from "../../../admin-console/models/data/provider.data"; import { PolicyResponse } from "../../../admin-console/models/response/policy.response"; +import { AccountService } from "../../../auth/abstractions/account.service"; import { AvatarService } from "../../../auth/abstractions/avatar.service"; import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service"; +import { InternalMasterPasswordServiceAbstraction } from "../../../auth/abstractions/master-password.service.abstraction"; +import { TokenService } from "../../../auth/abstractions/token.service"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { DomainSettingsService } from "../../../autofill/services/domain-settings.service"; import { BillingAccountProfileStateService } from "../../../billing/abstractions/account/billing-account-profile-state.service"; @@ -49,6 +52,8 @@ export class SyncService implements SyncServiceAbstraction { syncInProgress = false; constructor( + private masterPasswordService: InternalMasterPasswordServiceAbstraction, + private accountService: AccountService, private apiService: ApiService, private domainSettingsService: DomainSettingsService, private folderService: InternalFolderService, @@ -69,6 +74,7 @@ export class SyncService implements SyncServiceAbstraction { private avatarService: AvatarService, private logoutCallback: (expired: boolean) => Promise, private billingAccountProfileStateService: BillingAccountProfileStateService, + private tokenService: TokenService, ) {} async getLastSync(): Promise { @@ -305,7 +311,7 @@ export class SyncService implements SyncServiceAbstraction { } private async syncProfile(response: ProfileResponse) { - const stamp = await this.stateService.getSecurityStamp(); + const stamp = await this.tokenService.getSecurityStamp(response.id as UserId); if (stamp != null && stamp !== response.securityStamp) { if (this.logoutCallback != null) { await this.logoutCallback(true); @@ -319,7 +325,7 @@ export class SyncService implements SyncServiceAbstraction { await this.cryptoService.setProviderKeys(response.providers); await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations); await this.avatarService.setSyncAvatarColor(response.id as UserId, response.avatarColor); - await this.stateService.setSecurityStamp(response.securityStamp); + await this.tokenService.setSecurityStamp(response.securityStamp, response.id as UserId); await this.stateService.setEmailVerified(response.emailVerified); await this.billingAccountProfileStateService.setHasPremium( @@ -352,8 +358,10 @@ export class SyncService implements SyncServiceAbstraction { private async setForceSetPasswordReasonIfNeeded(profileResponse: ProfileResponse) { // The `forcePasswordReset` flag indicates an admin has reset the user's password and must be updated if (profileResponse.forcePasswordReset) { - await this.stateService.setForceSetPasswordReason( + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( ForceSetPasswordReason.AdminForcePasswordReset, + userId, ); } @@ -387,8 +395,10 @@ export class SyncService implements SyncServiceAbstraction { ) { // TDE user w/out MP went from having no password reset permission to having it. // Must set the force password reset reason so the auth guard will redirect to the set password page. - await this.stateService.setForceSetPasswordReason( + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, + userId, ); } } diff --git a/libs/components/src/avatar/avatar.component.ts b/libs/components/src/avatar/avatar.component.ts index 429e9fc0c6..b19506952d 100644 --- a/libs/components/src/avatar/avatar.component.ts +++ b/libs/components/src/avatar/avatar.component.ts @@ -40,7 +40,7 @@ export class AvatarComponent implements OnChanges { get classList() { return ["tw-rounded-full"] .concat(SizeClasses[this.size] ?? []) - .concat(this.border ? ["tw-border", "tw-border-solid", "tw-border-secondary-500"] : []); + .concat(this.border ? ["tw-border", "tw-border-solid", "tw-border-secondary-600"] : []); } private generate() { diff --git a/libs/components/src/avatar/avatar.mdx b/libs/components/src/avatar/avatar.mdx index c6c5ff78ba..0f3f6f06a9 100644 --- a/libs/components/src/avatar/avatar.mdx +++ b/libs/components/src/avatar/avatar.mdx @@ -44,7 +44,7 @@ Use the user 'ID' field if `Name` is not defined. ## Outline If the avatar is displayed on one of the theme's `background` color variables or is interactive, -display the avatar with a 1 pixel `secondary-500` border to meet WCAG AA graphic contrast guidelines +display the avatar with a 1 pixel `secondary-600` border to meet WCAG AA graphic contrast guidelines for interactive elements. @@ -64,4 +64,4 @@ When the avatar is used as a button, the following states should be used: ## Accessibility Avatar background color should have 3.1:1 contrast with it’s background; or include the -`secondary-500` border Avatar text should have 4.5:1 contrast with the avatar background color +`secondary-600` border Avatar text should have 4.5:1 contrast with the avatar background color diff --git a/libs/components/src/badge/badge.directive.ts b/libs/components/src/badge/badge.directive.ts index dd28d86ae8..b81b9f80e2 100644 --- a/libs/components/src/badge/badge.directive.ts +++ b/libs/components/src/badge/badge.directive.ts @@ -3,12 +3,12 @@ import { Directive, ElementRef, HostBinding, Input } from "@angular/core"; export type BadgeVariant = "primary" | "secondary" | "success" | "danger" | "warning" | "info"; const styles: Record = { - primary: ["tw-bg-primary-500"], + primary: ["tw-bg-primary-600"], secondary: ["tw-bg-text-muted"], - success: ["tw-bg-success-500"], - danger: ["tw-bg-danger-500"], - warning: ["tw-bg-warning-500"], - info: ["tw-bg-info-500"], + success: ["tw-bg-success-600"], + danger: ["tw-bg-danger-600"], + warning: ["tw-bg-warning-600"], + info: ["tw-bg-info-600"], }; const hoverStyles: Record = { diff --git a/libs/components/src/banner/banner.component.ts b/libs/components/src/banner/banner.component.ts index e93bcb2214..099fa11fa4 100644 --- a/libs/components/src/banner/banner.component.ts +++ b/libs/components/src/banner/banner.component.ts @@ -28,13 +28,13 @@ export class BannerComponent implements OnInit { get bannerClass() { switch (this.bannerType) { case "danger": - return "tw-bg-danger-500"; + return "tw-bg-danger-600"; case "info": - return "tw-bg-info-500"; + return "tw-bg-info-600"; case "premium": - return "tw-bg-success-500"; + return "tw-bg-success-600"; case "warning": - return "tw-bg-warning-500"; + return "tw-bg-warning-600"; } } } diff --git a/libs/components/src/button/button.component.spec.ts b/libs/components/src/button/button.component.spec.ts index a0b9c1e7a5..a75ac400a9 100644 --- a/libs/components/src/button/button.component.spec.ts +++ b/libs/components/src/button/button.component.spec.ts @@ -30,8 +30,8 @@ describe("Button", () => { it("should apply classes based on type", () => { testAppComponent.buttonType = "primary"; fixture.detectChanges(); - expect(buttonDebugElement.nativeElement.classList.contains("tw-bg-primary-500")).toBe(true); - expect(linkDebugElement.nativeElement.classList.contains("tw-bg-primary-500")).toBe(true); + expect(buttonDebugElement.nativeElement.classList.contains("tw-bg-primary-600")).toBe(true); + expect(linkDebugElement.nativeElement.classList.contains("tw-bg-primary-600")).toBe(true); testAppComponent.buttonType = "secondary"; fixture.detectChanges(); @@ -40,8 +40,8 @@ describe("Button", () => { testAppComponent.buttonType = "danger"; fixture.detectChanges(); - expect(buttonDebugElement.nativeElement.classList.contains("tw-border-danger-500")).toBe(true); - expect(linkDebugElement.nativeElement.classList.contains("tw-border-danger-500")).toBe(true); + expect(buttonDebugElement.nativeElement.classList.contains("tw-border-danger-600")).toBe(true); + expect(linkDebugElement.nativeElement.classList.contains("tw-border-danger-600")).toBe(true); testAppComponent.buttonType = "unstyled"; fixture.detectChanges(); diff --git a/libs/components/src/button/button.component.ts b/libs/components/src/button/button.component.ts index 414d4e5913..3cbacb4731 100644 --- a/libs/components/src/button/button.component.ts +++ b/libs/components/src/button/button.component.ts @@ -12,13 +12,13 @@ const focusRing = [ const buttonStyles: Record = { primary: [ - "tw-border-primary-500", - "tw-bg-primary-500", + "tw-border-primary-600", + "tw-bg-primary-600", "!tw-text-contrast", "hover:tw-bg-primary-700", "hover:tw-border-primary-700", - "disabled:tw-bg-primary-500/60", - "disabled:tw-border-primary-500/60", + "disabled:tw-bg-primary-600/60", + "disabled:tw-border-primary-600/60", "disabled:!tw-text-contrast/60", "disabled:tw-bg-clip-padding", "disabled:tw-cursor-not-allowed", @@ -39,13 +39,13 @@ const buttonStyles: Record = { ], danger: [ "tw-bg-transparent", - "tw-border-danger-500", + "tw-border-danger-600", "!tw-text-danger", - "hover:tw-bg-danger-500", - "hover:tw-border-danger-500", + "hover:tw-bg-danger-600", + "hover:tw-border-danger-600", "hover:!tw-text-contrast", "disabled:tw-bg-transparent", - "disabled:tw-border-danger-500/60", + "disabled:tw-border-danger-600/60", "disabled:!tw-text-danger/60", "disabled:tw-cursor-not-allowed", ...focusRing, diff --git a/libs/components/src/callout/callout.component.ts b/libs/components/src/callout/callout.component.ts index 7ce79071bf..6942d4bc15 100644 --- a/libs/components/src/callout/callout.component.ts +++ b/libs/components/src/callout/callout.component.ts @@ -42,13 +42,13 @@ export class CalloutComponent implements OnInit { get calloutClass() { switch (this.type) { case "danger": - return "tw-border-l-danger-500"; + return "tw-border-l-danger-600"; case "info": - return "tw-border-l-info-500"; + return "tw-border-l-info-600"; case "success": - return "tw-border-l-success-500"; + return "tw-border-l-success-600"; case "warning": - return "tw-border-l-warning-500"; + return "tw-border-l-warning-600"; } } diff --git a/libs/components/src/checkbox/checkbox.component.ts b/libs/components/src/checkbox/checkbox.component.ts index bbc288659c..d8fd3f76ea 100644 --- a/libs/components/src/checkbox/checkbox.component.ts +++ b/libs/components/src/checkbox/checkbox.component.ts @@ -20,7 +20,7 @@ export class CheckboxComponent implements BitFormControlAbstraction { "tw-rounded", "tw-border", "tw-border-solid", - "tw-border-secondary-500", + "tw-border-secondary-600", "tw-h-3.5", "tw-w-3.5", "tw-mr-1.5", @@ -43,8 +43,8 @@ export class CheckboxComponent implements BitFormControlAbstraction { "disabled:tw-border", "disabled:tw-bg-secondary-100", - "checked:tw-bg-primary-500", - "checked:tw-border-primary-500", + "checked:tw-bg-primary-600", + "checked:tw-border-primary-600", "checked:hover:tw-bg-primary-700", "checked:hover:tw-border-primary-700", "[&>label:hover]:checked:tw-bg-primary-700", @@ -59,8 +59,8 @@ export class CheckboxComponent implements BitFormControlAbstraction { "[&:not(:indeterminate)]:checked:before:tw-mask-image-[var(--mask-image)]", "indeterminate:before:tw-mask-image-[var(--indeterminate-mask-image)]", - "indeterminate:tw-bg-primary-500", - "indeterminate:tw-border-primary-500", + "indeterminate:tw-bg-primary-600", + "indeterminate:tw-border-primary-600", "indeterminate:hover:tw-bg-primary-700", "indeterminate:hover:tw-border-primary-700", "[&>label:hover]:indeterminate:tw-bg-primary-700", diff --git a/libs/components/src/color-password/color-password.component.ts b/libs/components/src/color-password/color-password.component.ts index 4c32d0af0d..efa2ab687f 100644 --- a/libs/components/src/color-password/color-password.component.ts +++ b/libs/components/src/color-password/color-password.component.ts @@ -30,7 +30,7 @@ export class ColorPasswordComponent { [CharacterType.Emoji]: [], [CharacterType.Letter]: ["tw-text-main"], [CharacterType.Special]: ["tw-text-danger"], - [CharacterType.Number]: ["tw-text-primary-500"], + [CharacterType.Number]: ["tw-text-primary-600"], }; @HostBinding("class") diff --git a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts index e93498ec48..1438c7926e 100644 --- a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts +++ b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts @@ -15,7 +15,7 @@ const DEFAULT_ICON: Record = { }; const DEFAULT_COLOR: Record = { - primary: "tw-text-primary-500", + primary: "tw-text-primary-600", success: "tw-text-success", info: "tw-text-info", warning: "tw-text-warning", diff --git a/libs/components/src/dialog/simple-dialog/simple-dialog.mdx b/libs/components/src/dialog/simple-dialog/simple-dialog.mdx index 5e45b7bef6..a78ba4650a 100644 --- a/libs/components/src/dialog/simple-dialog/simple-dialog.mdx +++ b/libs/components/src/dialog/simple-dialog/simple-dialog.mdx @@ -33,11 +33,11 @@ the simple dialog's type is specified. | type | icon name | icon | color | | ------- | ------------------------ | -------------------------------------------- | ----------- | -| primary | bwi-business | | primary-500 | -| success | bwi-star | | success-500 | -| info | bwi-info-circle | | info-500 | -| warning | bwi-exclamation-triangle | | warning-500 | -| danger | bwi-error | | danger-500 | +| primary | bwi-business | | primary-600 | +| success | bwi-star | | success-600 | +| info | bwi-info-circle | | info-600 | +| warning | bwi-exclamation-triangle | | warning-600 | +| danger | bwi-error | | danger-600 | ## Scrolling Content diff --git a/libs/components/src/form-field/prefix.directive.ts b/libs/components/src/form-field/prefix.directive.ts index 62643c8bb7..6e1e15fd20 100644 --- a/libs/components/src/form-field/prefix.directive.ts +++ b/libs/components/src/form-field/prefix.directive.ts @@ -6,7 +6,7 @@ export const PrefixClasses = [ "tw-bg-background-alt", "tw-border", "tw-border-solid", - "tw-border-secondary-500", + "tw-border-secondary-600", "tw-text-muted", "tw-rounded-none", ]; diff --git a/libs/components/src/form/forms.mdx b/libs/components/src/form/forms.mdx index 0845156561..a42ddccbe6 100644 --- a/libs/components/src/form/forms.mdx +++ b/libs/components/src/form/forms.mdx @@ -174,5 +174,5 @@ the field’s label. - All field inputs are interactive elements that must follow the WCAG graphic contrast guidelines. Maintain a ratio of 3:1 with the form's background. -- Error styling should not rely only on using the `danger-500`color change. Use +- Error styling should not rely only on using the `danger-600`color change. Use as a prefix to highlight the text as error text versus helper diff --git a/libs/components/src/icon-button/icon-button.component.ts b/libs/components/src/icon-button/icon-button.component.ts index 73872926f8..53e8032795 100644 --- a/libs/components/src/icon-button/icon-button.component.ts +++ b/libs/components/src/icon-button/icon-button.component.ts @@ -60,15 +60,15 @@ const styles: Record = { ...focusRing, ], primary: [ - "tw-bg-primary-500", + "tw-bg-primary-600", "!tw-text-contrast", - "tw-border-primary-500", + "tw-border-primary-600", "hover:tw-bg-primary-700", "hover:tw-border-primary-700", "focus-visible:before:tw-ring-primary-700", "disabled:tw-opacity-60", - "disabled:hover:tw-border-primary-500", - "disabled:hover:tw-bg-primary-500", + "disabled:hover:tw-border-primary-600", + "disabled:hover:tw-bg-primary-600", ...focusRing, ], secondary: [ @@ -88,15 +88,15 @@ const styles: Record = { danger: [ "tw-bg-transparent", "!tw-text-danger", - "tw-border-danger-500", + "tw-border-danger-600", "hover:!tw-text-contrast", - "hover:tw-bg-danger-500", + "hover:tw-bg-danger-600", "focus-visible:before:tw-ring-primary-700", "disabled:tw-opacity-60", - "disabled:hover:tw-border-danger-500", + "disabled:hover:tw-border-danger-600", "disabled:hover:tw-bg-transparent", "disabled:hover:!tw-text-danger", - "disabled:hover:tw-border-danger-500", + "disabled:hover:tw-border-danger-600", ...focusRing, ], light: [ diff --git a/libs/components/src/icon-button/icon-button.stories.ts b/libs/components/src/icon-button/icon-button.stories.ts index 19bc972a70..0f25d2de58 100644 --- a/libs/components/src/icon-button/icon-button.stories.ts +++ b/libs/components/src/icon-button/icon-button.stories.ts @@ -30,7 +30,7 @@ export const Default: Story = { -
    +
    @@ -111,7 +111,7 @@ export const Contrast: Story = { render: (args) => ({ props: args, template: ` -
    +
    `, diff --git a/libs/components/src/icon/icons/no-access.ts b/libs/components/src/icon/icons/no-access.ts index f9ad048752..1011b3089c 100644 --- a/libs/components/src/icon/icons/no-access.ts +++ b/libs/components/src/icon/icons/no-access.ts @@ -2,11 +2,11 @@ import { svgIcon } from "../icon"; export const NoAccess = svgIcon` - - - - - - + + + + + + `; diff --git a/libs/components/src/icon/icons/search.ts b/libs/components/src/icon/icons/search.ts index de41dd3b19..914fa0e981 100644 --- a/libs/components/src/icon/icons/search.ts +++ b/libs/components/src/icon/icons/search.ts @@ -4,15 +4,15 @@ export const Search = svgIcon` - - - - - - - - - + + + + + + + + + `; diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 139e69ebb6..527d5f3615 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -29,6 +29,7 @@ export * from "./section"; export * from "./select"; export * from "./table"; export * from "./tabs"; +export * from "./toast"; export * from "./toggle-group"; export * from "./typography"; export * from "./utils/i18n-mock.service"; diff --git a/libs/components/src/input/input.directive.ts b/libs/components/src/input/input.directive.ts index 9bd110704e..27c7d8175d 100644 --- a/libs/components/src/input/input.directive.ts +++ b/libs/components/src/input/input.directive.ts @@ -29,7 +29,7 @@ export class BitInputDirective implements BitFormFieldControl { "tw-bg-background-alt", "tw-border", "tw-border-solid", - this.hasError ? "tw-border-danger-500" : "tw-border-secondary-500", + this.hasError ? "tw-border-danger-600" : "tw-border-secondary-600", "tw-text-main", "tw-placeholder-text-muted", // Rounded diff --git a/libs/components/src/link/link.directive.ts b/libs/components/src/link/link.directive.ts index 6d923acf3d..a8ee528da1 100644 --- a/libs/components/src/link/link.directive.ts +++ b/libs/components/src/link/link.directive.ts @@ -4,10 +4,10 @@ export type LinkType = "primary" | "secondary" | "contrast" | "light"; const linkStyles: Record = { primary: [ - "!tw-text-primary-500", - "hover:!tw-text-primary-500", + "!tw-text-primary-600", + "hover:!tw-text-primary-600", "focus-visible:before:tw-ring-primary-700", - "disabled:!tw-text-primary-500/60", + "disabled:!tw-text-primary-600/60", ], secondary: [ "!tw-text-main", diff --git a/libs/components/src/link/link.mdx b/libs/components/src/link/link.mdx index 48c8c2abd5..100824277a 100644 --- a/libs/components/src/link/link.mdx +++ b/libs/components/src/link/link.mdx @@ -6,7 +6,7 @@ import * as stories from "./link.stories"; # Link / Text button -Text Links and Buttons use the `primary-500` color and can use either the `` or `
    @@ -61,7 +61,7 @@ export const Anchors: StoryObj = { render: (args) => ({ props: args, template: ` -
    +
    @@ -108,7 +108,7 @@ export const Disabled: Story = { template: ` -
    +
    `, diff --git a/libs/components/src/menu/menu-divider.component.html b/libs/components/src/menu/menu-divider.component.html index 98048261cf..7d9fee3e8f 100644 --- a/libs/components/src/menu/menu-divider.component.html +++ b/libs/components/src/menu/menu-divider.component.html @@ -1,5 +1,5 @@ diff --git a/libs/components/src/menu/menu-item.directive.ts b/libs/components/src/menu/menu-item.directive.ts index 2a50dd366f..77246bbcdf 100644 --- a/libs/components/src/menu/menu-item.directive.ts +++ b/libs/components/src/menu/menu-item.directive.ts @@ -16,12 +16,12 @@ export class MenuItemDirective implements FocusableOption { "tw-bg-background", "tw-text-left", "hover:tw-bg-secondary-100", - "focus:tw-bg-secondary-100", - "focus:tw-z-50", - "focus:tw-outline-none", - "focus:tw-ring", - "focus:tw-ring-offset-2", - "focus:tw-ring-primary-700", + "focus-visible:tw-bg-secondary-100", + "focus-visible:tw-z-50", + "focus-visible:tw-outline-none", + "focus-visible:tw-ring", + "focus-visible:tw-ring-offset-2", + "focus-visible:tw-ring-primary-700", "active:!tw-ring-0", "active:!tw-ring-offset-0", ]; diff --git a/libs/components/src/menu/menu.component.html b/libs/components/src/menu/menu.component.html index 98a35e97de..5b6b15b5cb 100644 --- a/libs/components/src/menu/menu.component.html +++ b/libs/components/src/menu/menu.component.html @@ -1,7 +1,7 @@
    +
    +
    +
    diff --git a/libs/components/src/toast/toast.component.ts b/libs/components/src/toast/toast.component.ts new file mode 100644 index 0000000000..4a31e00586 --- /dev/null +++ b/libs/components/src/toast/toast.component.ts @@ -0,0 +1,66 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; + +import { IconButtonModule } from "../icon-button"; +import { SharedModule } from "../shared"; + +export type ToastVariant = "success" | "error" | "info" | "warning"; + +const variants: Record = { + success: { + icon: "bwi-check", + bgColor: "tw-bg-success-600", + }, + error: { + icon: "bwi-error", + bgColor: "tw-bg-danger-600", + }, + info: { + icon: "bwi-info-circle", + bgColor: "tw-bg-info-600", + }, + warning: { + icon: "bwi-exclamation-triangle", + bgColor: "tw-bg-warning-600", + }, +}; + +@Component({ + selector: "bit-toast", + templateUrl: "toast.component.html", + standalone: true, + imports: [SharedModule, IconButtonModule], +}) +export class ToastComponent { + @Input() variant: ToastVariant = "info"; + + /** + * The message to display + * + * Pass an array to render multiple paragraphs. + **/ + @Input({ required: true }) + message: string | string[]; + + /** An optional title to display over the message. */ + @Input() title: string; + + /** + * The percent width of the progress bar, from 0-100 + **/ + @Input() progressWidth = 0; + + /** Emits when the user presses the close button */ + @Output() onClose = new EventEmitter(); + + protected get iconClass(): string { + return variants[this.variant].icon; + } + + protected get bgColor(): string { + return variants[this.variant].bgColor; + } + + protected get messageArray(): string[] { + return Array.isArray(this.message) ? this.message : [this.message]; + } +} diff --git a/libs/components/src/toast/toast.module.ts b/libs/components/src/toast/toast.module.ts new file mode 100644 index 0000000000..bf39a0be9a --- /dev/null +++ b/libs/components/src/toast/toast.module.ts @@ -0,0 +1,39 @@ +import { CommonModule } from "@angular/common"; +import { ModuleWithProviders, NgModule } from "@angular/core"; +import { DefaultNoComponentGlobalConfig, GlobalConfig, TOAST_CONFIG } from "ngx-toastr"; + +import { ToastComponent } from "./toast.component"; +import { BitwardenToastrComponent } from "./toastr.component"; + +@NgModule({ + imports: [CommonModule, ToastComponent], + declarations: [BitwardenToastrComponent], + exports: [BitwardenToastrComponent], +}) +export class ToastModule { + static forRoot(config: Partial = {}): ModuleWithProviders { + return { + ngModule: ToastModule, + providers: [ + { + provide: TOAST_CONFIG, + useValue: { + default: BitwardenToastrGlobalConfig, + config: config, + }, + }, + ], + }; + } +} + +export const BitwardenToastrGlobalConfig: GlobalConfig = { + ...DefaultNoComponentGlobalConfig, + toastComponent: BitwardenToastrComponent, + tapToDismiss: false, + timeOut: 5000, + extendedTimeOut: 2000, + maxOpened: 5, + autoDismiss: true, + progressBar: true, +}; diff --git a/libs/components/src/toast/toast.service.ts b/libs/components/src/toast/toast.service.ts new file mode 100644 index 0000000000..8bbff02c41 --- /dev/null +++ b/libs/components/src/toast/toast.service.ts @@ -0,0 +1,57 @@ +import { Injectable } from "@angular/core"; +import { IndividualConfig, ToastrService } from "ngx-toastr"; + +import type { ToastComponent } from "./toast.component"; +import { calculateToastTimeout } from "./utils"; + +export type ToastOptions = { + /** + * The duration the toast will persist in milliseconds + **/ + timeout?: number; +} & Pick; + +/** + * Presents toast notifications + **/ +@Injectable({ providedIn: "root" }) +export class ToastService { + constructor(private toastrService: ToastrService) {} + + showToast(options: ToastOptions) { + const toastrConfig: Partial = { + payload: { + message: options.message, + variant: options.variant, + title: options.title, + }, + timeOut: + options.timeout != null && options.timeout > 0 + ? options.timeout + : calculateToastTimeout(options.message), + }; + + this.toastrService.show(null, options.title, toastrConfig); + } + + /** + * @deprecated use `showToast` instead + * + * Converts options object from PlatformUtilsService + **/ + _showToast(options: { + type: "error" | "success" | "warning" | "info"; + title: string; + text: string | string[]; + options?: { + timeout?: number; + }; + }) { + this.showToast({ + message: options.text, + variant: options.type, + title: options.title, + timeout: options.options?.timeout, + }); + } +} diff --git a/libs/components/src/toast/toast.spec.ts b/libs/components/src/toast/toast.spec.ts new file mode 100644 index 0000000000..92d8071dc5 --- /dev/null +++ b/libs/components/src/toast/toast.spec.ts @@ -0,0 +1,16 @@ +import { calculateToastTimeout } from "./utils"; + +describe("Toast default timer", () => { + it("should have a minimum of 5000ms", () => { + expect(calculateToastTimeout("")).toBe(5000); + expect(calculateToastTimeout([""])).toBe(5000); + expect(calculateToastTimeout(" ")).toBe(5000); + }); + + it("should return an extra second for each 120 words", () => { + expect(calculateToastTimeout("foo ".repeat(119))).toBe(5000); + expect(calculateToastTimeout("foo ".repeat(120))).toBe(6000); + expect(calculateToastTimeout("foo ".repeat(240))).toBe(7000); + expect(calculateToastTimeout(["foo ".repeat(120), " \n foo ".repeat(120)])).toBe(7000); + }); +}); diff --git a/libs/components/src/toast/toast.stories.ts b/libs/components/src/toast/toast.stories.ts new file mode 100644 index 0000000000..d209453d85 --- /dev/null +++ b/libs/components/src/toast/toast.stories.ts @@ -0,0 +1,124 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { action } from "@storybook/addon-actions"; +import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { ButtonModule } from "../button"; +import { I18nMockService } from "../utils/i18n-mock.service"; + +import { ToastComponent } from "./toast.component"; +import { BitwardenToastrGlobalConfig, ToastModule } from "./toast.module"; +import { ToastOptions, ToastService } from "./toast.service"; + +const toastServiceExampleTemplate = ` + +`; +@Component({ + selector: "toast-service-example", + template: toastServiceExampleTemplate, +}) +export class ToastServiceExampleComponent { + @Input() + toastOptions: ToastOptions; + + constructor(protected toastService: ToastService) {} +} + +export default { + title: "Component Library/Toast", + component: ToastComponent, + + decorators: [ + moduleMetadata({ + imports: [CommonModule, BrowserAnimationsModule, ButtonModule], + declarations: [ToastServiceExampleComponent], + }), + applicationConfig({ + providers: [ + ToastModule.forRoot().providers, + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + close: "Close", + success: "Success", + error: "Error", + warning: "Warning", + }); + }, + }, + ], + }), + ], + args: { + onClose: action("emit onClose"), + variant: "info", + progressWidth: 50, + title: "", + message: "Hello Bitwarden!", + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library", + }, + }, +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ({ + props: args, + template: ` +
    + + + + +
    + `, + }), +}; + +/** + * Avoid using long messages in toasts. + */ +export const LongContent: Story = { + ...Default, + args: { + title: "Foo", + message: [ + "Lorem ipsum dolor sit amet, consectetur adipisci", + "Lorem ipsum dolor sit amet, consectetur adipisci", + ], + }, +}; + +export const Service: Story = { + render: (args) => ({ + props: { + toastOptions: args, + }, + template: ` + + `, + }), + args: { + title: "", + message: "Hello Bitwarden!", + variant: "error", + timeout: BitwardenToastrGlobalConfig.timeOut, + } as ToastOptions, + parameters: { + chromatic: { disableSnapshot: true }, + docs: { + source: { + code: toastServiceExampleTemplate, + }, + }, + }, +}; diff --git a/libs/components/src/toast/toast.tokens.css b/libs/components/src/toast/toast.tokens.css new file mode 100644 index 0000000000..2ff9e99ae5 --- /dev/null +++ b/libs/components/src/toast/toast.tokens.css @@ -0,0 +1,4 @@ +:root { + --bit-toast-width: 19rem; + --bit-toast-width-full: 96%; +} diff --git a/libs/components/src/toast/toastr.component.ts b/libs/components/src/toast/toastr.component.ts new file mode 100644 index 0000000000..70085dfc47 --- /dev/null +++ b/libs/components/src/toast/toastr.component.ts @@ -0,0 +1,26 @@ +import { animate, state, style, transition, trigger } from "@angular/animations"; +import { Component } from "@angular/core"; +import { Toast as BaseToastrComponent } from "ngx-toastr"; + +@Component({ + template: ` + + `, + animations: [ + trigger("flyInOut", [ + state("inactive", style({ opacity: 0 })), + state("active", style({ opacity: 1 })), + state("removed", style({ opacity: 0 })), + transition("inactive => active", animate("{{ easeTime }}ms {{ easing }}")), + transition("active => removed", animate("{{ easeTime }}ms {{ easing }}")), + ]), + ], + preserveWhitespaces: false, +}) +export class BitwardenToastrComponent extends BaseToastrComponent {} diff --git a/libs/components/src/toast/toastr.css b/libs/components/src/toast/toastr.css new file mode 100644 index 0000000000..fabf8caf10 --- /dev/null +++ b/libs/components/src/toast/toastr.css @@ -0,0 +1,23 @@ +@import "~ngx-toastr/toastr"; +@import "./toast.tokens.css"; + +/* Override all default styles from `ngx-toaster` */ +.toast-container .ngx-toastr { + all: unset; + display: block; + width: var(--bit-toast-width); + + /* Needed to make hover states work in Electron, since the toast appears in the draggable region. */ + -webkit-app-region: no-drag; +} + +/* Disable hover styles */ +.toast-container .ngx-toastr:hover { + box-shadow: none; +} + +.toast-container.toast-bottom-full-width .ngx-toastr { + width: var(--bit-toast-width-full); + margin-left: auto; + margin-right: auto; +} diff --git a/libs/components/src/toast/utils.ts b/libs/components/src/toast/utils.ts new file mode 100644 index 0000000000..4c8323f396 --- /dev/null +++ b/libs/components/src/toast/utils.ts @@ -0,0 +1,14 @@ +/** + * Given a toast message, calculate the ideal timeout length following: + * a minimum of 5 seconds + 1 extra second per 120 additional words + * + * @param message the toast message to be displayed + * @returns the timeout length in milliseconds + */ +export const calculateToastTimeout = (message: string | string[]): number => { + const paragraphs = Array.isArray(message) ? message : [message]; + const numWords = paragraphs + .map((paragraph) => paragraph.split(/\s+/).filter((word) => word !== "")) + .flat().length; + return 5000 + Math.floor(numWords / 120) * 1000; +}; diff --git a/libs/components/src/toggle-group/toggle.component.ts b/libs/components/src/toggle-group/toggle.component.ts index 55c678d017..7d227acde3 100644 --- a/libs/components/src/toggle-group/toggle.component.ts +++ b/libs/components/src/toggle-group/toggle.component.ts @@ -50,10 +50,10 @@ export class ToggleComponent { "peer-focus:tw-outline-none", "peer-focus:tw-ring", "peer-focus:tw-ring-offset-2", - "peer-focus:tw-ring-primary-500", + "peer-focus:tw-ring-primary-600", "peer-focus:tw-z-10", - "peer-focus:tw-bg-primary-500", - "peer-focus:tw-border-primary-500", + "peer-focus:tw-bg-primary-600", + "peer-focus:tw-border-primary-600", "peer-focus:!tw-text-contrast", "hover:tw-no-underline", @@ -61,8 +61,8 @@ export class ToggleComponent { "hover:tw-border-text-muted", "hover:!tw-text-contrast", - "peer-checked:tw-bg-primary-500", - "peer-checked:tw-border-primary-500", + "peer-checked:tw-bg-primary-600", + "peer-checked:tw-border-primary-600", "peer-checked:!tw-text-contrast", "tw-py-1.5", "tw-px-3", diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css index 75a8fa6380..72e8e1e5e8 100644 --- a/libs/components/src/tw-theme.css +++ b/libs/components/src/tw-theme.css @@ -1,5 +1,13 @@ @import "./reset.css"; +/** + Note that the value of the *-600 colors is currently equivalent to the value + of the *-500 variant of that color. This is a temporary change to make BW-42 + updates easier. + + TODO remove comment when the color palette portion of BW-42 is completed. +*/ + :root { --color-transparent-hover: rgb(0 0 0 / 0.03); @@ -10,24 +18,24 @@ --color-background-alt4: 13 60 119; --color-primary-300: 103 149 232; - --color-primary-500: 23 93 220; + --color-primary-600: 23 93 220; --color-primary-700: 18 82 163; --color-secondary-100: 240 240 240; --color-secondary-300: 206 212 220; - --color-secondary-500: 137 146 159; + --color-secondary-600: 137 146 159; --color-secondary-700: 33 37 41; - --color-success-500: 1 126 69; + --color-success-600: 1 126 69; --color-success-700: 0 85 46; - --color-danger-500: 200 53 34; + --color-danger-600: 200 53 34; --color-danger-700: 152 41 27; - --color-warning-500: 139 102 9; + --color-warning-600: 139 102 9; --color-warning-700: 105 77 5; - --color-info-500: 85 85 85; + --color-info-600: 85 85 85; --color-info-700: 59 58 58; --color-text-main: 33 37 41; @@ -53,24 +61,24 @@ --color-background-alt4: 16 18 21; --color-primary-300: 23 93 220; - --color-primary-500: 106 153 240; + --color-primary-600: 106 153 240; --color-primary-700: 180 204 249; --color-secondary-100: 47 52 61; --color-secondary-300: 110 118 137; - --color-secondary-500: 186 192 206; + --color-secondary-600: 186 192 206; --color-secondary-700: 255 255 255; - --color-success-500: 82 224 124; + --color-success-600: 82 224 124; --color-success-700: 168 239 190; - --color-danger-500: 255 141 133; + --color-danger-600: 255 141 133; --color-danger-700: 255 191 187; - --color-warning-500: 255 235 102; + --color-warning-600: 255 235 102; --color-warning-700: 255 245 179; - --color-info-500: 164 176 198; + --color-info-600: 164 176 198; --color-info-700: 209 215 226; --color-text-main: 255 255 255; @@ -92,24 +100,24 @@ --color-background-alt4: 67 76 94; --color-primary-300: 108 153 166; - --color-primary-500: 136 192 208; + --color-primary-600: 136 192 208; --color-primary-700: 160 224 242; --color-secondary-100: 76 86 106; --color-secondary-300: 94 105 125; - --color-secondary-500: 216 222 233; + --color-secondary-600: 216 222 233; --color-secondary-700: 255 255 255; - --color-success-500: 163 190 140; + --color-success-600: 163 190 140; --color-success-700: 144 170 122; - --color-danger-500: 228 129 139; + --color-danger-600: 228 129 139; --color-danger-700: 191 97 106; - --color-warning-500: 235 203 139; + --color-warning-600: 235 203 139; --color-warning-700: 210 181 121; - --color-info-500: 129 161 193; + --color-info-600: 129 161 193; --color-info-700: 94 129 172; --color-text-main: 229 233 240; @@ -131,24 +139,24 @@ --color-background-alt4: 0 43 54; --color-primary-300: 42 161 152; - --color-primary-500: 133 153 0; + --color-primary-600: 133 153 0; --color-primary-700: 192 203 123; --color-secondary-100: 31 72 87; --color-secondary-300: 101 123 131; - --color-secondary-500: 131 148 150; + --color-secondary-600: 131 148 150; --color-secondary-700: 238 232 213; - --color-success-500: 133 153 0; + --color-success-600: 133 153 0; --color-success-700: 192 203 123; - --color-danger-500: 220 50 47; + --color-danger-600: 220 50 47; --color-danger-700: 223 135 134; - --color-warning-500: 181 137 0; + --color-warning-600: 181 137 0; --color-warning-700: 220 189 92; - --color-info-500: 133 153 0; + --color-info-600: 133 153 0; --color-info-700: 192 203 123; --color-text-main: 253 246 227; @@ -163,6 +171,9 @@ @import "./popover/popover.component.css"; @import "./search/search.component.css"; +@import "./toast/toast.tokens.css"; +@import "./toast/toastr.css"; + /** * tw-break-words does not work with table cells: * https://github.com/tailwindlabs/tailwindcss/issues/835 diff --git a/libs/components/tailwind.config.base.js b/libs/components/tailwind.config.base.js index 5f49c6fc26..b76f25eae7 100644 --- a/libs/components/tailwind.config.base.js +++ b/libs/components/tailwind.config.base.js @@ -25,29 +25,29 @@ module.exports = { black: colors.black, primary: { 300: rgba("--color-primary-300"), - 500: rgba("--color-primary-500"), + 600: rgba("--color-primary-600"), 700: rgba("--color-primary-700"), }, secondary: { 100: rgba("--color-secondary-100"), 300: rgba("--color-secondary-300"), - 500: rgba("--color-secondary-500"), + 600: rgba("--color-secondary-600"), 700: rgba("--color-secondary-700"), }, success: { - 500: rgba("--color-success-500"), + 600: rgba("--color-success-600"), 700: rgba("--color-success-700"), }, danger: { - 500: rgba("--color-danger-500"), + 600: rgba("--color-danger-600"), 700: rgba("--color-danger-700"), }, warning: { - 500: rgba("--color-warning-500"), + 600: rgba("--color-warning-600"), 700: rgba("--color-warning-700"), }, info: { - 500: rgba("--color-info-500"), + 600: rgba("--color-info-600"), 700: rgba("--color-info-700"), }, text: { @@ -71,13 +71,13 @@ module.exports = { contrast: rgba("--color-text-contrast"), alt2: rgba("--color-text-alt2"), code: rgba("--color-text-code"), - success: rgba("--color-success-500"), - danger: rgba("--color-danger-500"), - warning: rgba("--color-warning-500"), - info: rgba("--color-info-500"), + success: rgba("--color-success-600"), + danger: rgba("--color-danger-600"), + warning: rgba("--color-warning-600"), + info: rgba("--color-info-600"), primary: { 300: rgba("--color-primary-300"), - 500: rgba("--color-primary-500"), + 600: rgba("--color-primary-600"), 700: rgba("--color-primary-700"), }, }, diff --git a/libs/importer/src/services/import.service.spec.ts b/libs/importer/src/services/import.service.spec.ts index eb21f384b5..7bbcd3287a 100644 --- a/libs/importer/src/services/import.service.spec.ts +++ b/libs/importer/src/services/import.service.spec.ts @@ -196,7 +196,7 @@ describe("ImportService", () => { new Object() as FolderView, ); - await expect(setImportTargetMethod).rejects.toThrow("Error assigning target collection"); + await expect(setImportTargetMethod).rejects.toThrow(); }); it("passing importTarget as null on setImportTarget throws error", async () => { @@ -206,7 +206,7 @@ describe("ImportService", () => { new Object() as CollectionView, ); - await expect(setImportTargetMethod).rejects.toThrow("Error assigning target folder"); + await expect(setImportTargetMethod).rejects.toThrow(); }); it("passing importTarget, collectionRelationship has the expected values", async () => { diff --git a/libs/importer/src/services/import.service.ts b/libs/importer/src/services/import.service.ts index 62961a77c4..f5cab933f3 100644 --- a/libs/importer/src/services/import.service.ts +++ b/libs/importer/src/services/import.service.ts @@ -432,7 +432,7 @@ export class ImportService implements ImportServiceAbstraction { if (organizationId) { if (!(importTarget instanceof CollectionView)) { - throw new Error("Error assigning target collection"); + throw new Error(this.i18nService.t("errorAssigningTargetCollection")); } const noCollectionRelationShips: [number, number][] = []; @@ -463,7 +463,7 @@ export class ImportService implements ImportServiceAbstraction { } if (!(importTarget instanceof FolderView)) { - throw new Error("Error assigning target folder"); + throw new Error(this.i18nService.t("errorAssigningTargetFolder")); } const noFolderRelationShips: [number, number][] = []; diff --git a/libs/shared/jest.config.angular.js b/libs/shared/jest.config.angular.js index a0dcc27516..689a04d858 100644 --- a/libs/shared/jest.config.angular.js +++ b/libs/shared/jest.config.angular.js @@ -6,6 +6,11 @@ const { defaultTransformerOptions } = require("jest-preset-angular/presets"); module.exports = { testMatch: ["**/+(*.)+(spec).+(ts)"], + testPathIgnorePatterns: [ + "/node_modules/", // default value + ".*.type.spec.ts", // ignore type tests (which are checked at compile time and not run by jest) + ], + // Workaround for a memory leak that crashes tests in CI: // https://github.com/facebook/jest/issues/9430#issuecomment-1149882002 // Also anecdotally improves performance when run locally diff --git a/package-lock.json b/package-lock.json index 4ec09e2b7c..d72ba9cb19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -127,7 +127,7 @@ "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "6.10.0", - "electron": "28.2.8", + "electron": "28.3.1", "electron-builder": "24.13.3", "electron-log": "5.0.1", "electron-reload": "2.0.0-alpha.1", @@ -168,7 +168,7 @@ "regedit": "^3.0.3", "remark-gfm": "3.0.1", "rimraf": "5.0.5", - "sass": "1.69.5", + "sass": "1.74.1", "sass-loader": "13.3.3", "storybook": "7.6.17", "style-loader": "3.3.4", @@ -193,7 +193,7 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2024.3.1" + "version": "2024.4.1" }, "apps/cli": { "name": "@bitwarden/cli", @@ -233,7 +233,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.3.2", + "version": "2024.4.2", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -247,7 +247,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2024.3.1" + "version": "2024.4.1" }, "libs/admin-console": { "name": "@bitwarden/admin-console", @@ -16924,9 +16924,9 @@ } }, "node_modules/electron": { - "version": "28.2.8", - "resolved": "https://registry.npmjs.org/electron/-/electron-28.2.8.tgz", - "integrity": "sha512-VgXw2OHqPJkobIC7X9eWh3atptjnELaP+zlbF9Oz00ridlaOWmtLPsp6OaXbLw35URpMr0iYesq8okKp7S0k+g==", + "version": "28.3.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-28.3.1.tgz", + "integrity": "sha512-aF9fONuhVDJlctJS7YOw76ynxVAQdfIWmlhRMKits24tDcdSL0eMHUS0wWYiRfGWbQnUKB6V49Rf17o32f4/fg==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -33467,9 +33467,9 @@ } }, "node_modules/sass": { - "version": "1.69.5", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", - "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==", + "version": "1.74.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.74.1.tgz", + "integrity": "sha512-w0Z9p/rWZWelb88ISOLyvqTWGmtmu2QJICqDBGyNnfG4OUnPX9BBjjYIXUpXCMOOg5MQWNpqzt876la1fsTvUA==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", diff --git a/package.json b/package.json index 1c36865cd6..057e737903 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "6.10.0", - "electron": "28.2.8", + "electron": "28.3.1", "electron-builder": "24.13.3", "electron-log": "5.0.1", "electron-reload": "2.0.0-alpha.1", @@ -129,7 +129,7 @@ "regedit": "^3.0.3", "remark-gfm": "3.0.1", "rimraf": "5.0.5", - "sass": "1.69.5", + "sass": "1.74.1", "sass-loader": "13.3.3", "storybook": "7.6.17", "style-loader": "3.3.4",