diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5ba5885d725..93693f183c3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -122,6 +122,9 @@ apps/cli/src/locales/en/messages.json apps/desktop/src/locales/en/messages.json apps/web/src/locales/en/messages.json +## Ssh agent temporary co-codeowner +apps/desktop/desktop_native/core/src/ssh_agent @bitwarden/team-platform-dev @bitwarden/wg-ssh-keys + ## BRE team owns these workflows ## .github/workflows/brew-bump-desktop.yml @bitwarden/dept-bre .github/workflows/deploy-web.yml @bitwarden/dept-bre diff --git a/.github/renovate.json b/.github/renovate.json index c9cfd548956..0172403f0f1 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -41,16 +41,12 @@ }, { "matchPackageNames": [ - "@ngtools/webpack", "base64-loader", "buffer", "bufferutil", - "copy-webpack-plugin", "core-js", "css-loader", "html-loader", - "html-webpack-injector", - "html-webpack-plugin", "mini-css-extract-plugin", "ngx-infinite-scroll", "postcss", @@ -60,20 +56,15 @@ "sass-loader", "style-loader", "ts-loader", - "tsconfig-paths-webpack-plugin", "url", - "util", - "webpack", - "webpack-cli", - "webpack-dev-server", - "webpack-node-externals" + "util" ], "description": "Admin Console owned dependencies", "commitMessagePrefix": "[deps] AC:", "reviewers": ["team:team-admin-console-dev"] }, { - "matchPackageNames": ["@types/node-ipc", "node-ipc", "qrious"], + "matchPackageNames": ["qrious"], "description": "Auth owned dependencies", "commitMessagePrefix": "[deps] Auth:", "reviewers": ["team:team-auth-dev"] @@ -110,27 +101,43 @@ }, { "matchPackageNames": [ + "@babel/core", + "@babel/preset-env", "@electron/notarize", "@electron/rebuild", - "@types/argon2-browser", + "@ngtools/webpack", "@types/chrome", "@types/firefox-webext-browser", + "@types/glob", "@types/jquery", + "@types/lowdb", "@types/node", "@types/node-forge", - "argon2", - "argon2-browser", - "big-integer", + "@types/node-ipc", + "@yao-pkg", + "babel-loader", + "browserslist", + "copy-webpack-plugin", + "electron", "electron-builder", "electron-log", "electron-reload", "electron-store", "electron-updater", - "electron", + "html-webpack-injector", + "html-webpack-plugin", + "lowdb", "node-forge", + "node-ipc", + "pkg", "rxjs", + "tsconfig-paths-webpack-plugin", "type-fest", - "typescript" + "typescript", + "webpack", + "webpack-cli", + "webpack-dev-server", + "webpack-node-externals" ], "description": "Platform owned dependencies", "commitMessagePrefix": "[deps] Platform:", @@ -231,7 +238,6 @@ "@types/koa__router", "@types/koa-bodyparser", "@types/koa-json", - "@types/lowdb", "@types/lunr", "@types/node-fetch", "@types/proper-lockfile", @@ -244,18 +250,22 @@ "koa", "koa-bodyparser", "koa-json", - "lowdb", "lunr", "multer", "node-fetch", "open", - "pkg", "proper-lockfile", "qrcode-parser" ], "description": "Vault owned dependencies", "commitMessagePrefix": "[deps] Vault:", "reviewers": ["team:team-vault-dev"] + }, + { + "matchPackageNames": ["@types/argon2-browser", "argon2", "argon2-browser", "big-integer"], + "description": "Key Management owned dependencies", + "commitMessagePrefix": "[deps] KM:", + "reviewers": ["team:team-key-management-dev"] } ], "ignoreDeps": ["@types/koa-bodyparser", "bootstrap", "node-ipc", "node", "npm"] diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 647f1980819..42d012d5a98 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -41,6 +41,8 @@ jobs: setup: name: Setup runs-on: ubuntu-22.04 + needs: + - check-run outputs: repo_url: ${{ steps.gen_vars.outputs.repo_url }} adj_build_number: ${{ steps.gen_vars.outputs.adj_build_number }} @@ -236,7 +238,6 @@ jobs: needs: - setup - locales-test - - check-run env: _BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} @@ -350,7 +351,7 @@ jobs: crowdin-push: name: Crowdin Push - if: github.ref == 'refs/heads/main' + if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' runs-on: ubuntu-22.04 needs: - build @@ -397,7 +398,10 @@ jobs: - crowdin-push steps: - name: Check if any job failed - if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure') + if: | + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-browser') + && contains(needs.*.result, 'failure') run: exit 1 - name: Login to Azure - Prod Subscription diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 1b0679dfbeb..ac39ab2608b 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -42,6 +42,8 @@ jobs: setup: name: Setup runs-on: ubuntu-22.04 + needs: + - check-run outputs: package_version: ${{ steps.retrieve-package-version.outputs.package_version }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} @@ -398,11 +400,13 @@ jobs: - cli - cli-windows - snap - - check-run steps: - name: Check if any job failed working-directory: ${{ github.workspace }} - if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure') + if: | + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-cli') + && contains(needs.*.result, 'failure') run: exit 1 - name: Login to Azure - Prod Subscription diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index ca64dbc0a4d..221c998247f 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -40,6 +40,8 @@ jobs: electron-verify: name: Verify Electron Version runs-on: ubuntu-22.04 + needs: + - check-run steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -61,6 +63,8 @@ jobs: setup: name: Setup runs-on: ubuntu-22.04 + needs: + - check-run outputs: package_version: ${{ steps.retrieve-version.outputs.package_version }} release_channel: ${{ steps.release-channel.outputs.channel }} @@ -251,7 +255,6 @@ jobs: runs-on: windows-2022 needs: - setup - - check-run defaults: run: shell: pwsh @@ -464,7 +467,6 @@ jobs: runs-on: macos-13 needs: - setup - - check-run env: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} @@ -1056,9 +1058,8 @@ jobs: - name: Deploy to TestFlight id: testflight-deploy if: | - (github.ref == 'refs/heads/main' - || github.ref == 'refs/heads/rc' - || github.ref == 'refs/heads/hotfix-rc-desktop') + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-desktop') env: APP_STORE_CONNECT_TEAM_ISSUER: ${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }} APP_STORE_CONNECT_AUTH_KEY: 6TV9MKN3GP @@ -1073,9 +1074,8 @@ jobs: - name: Post message to a Slack channel id: slack-message if: | - (github.ref == 'refs/heads/main' - || github.ref == 'refs/heads/rc' - || github.ref == 'refs/heads/hotfix-rc-desktop') + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-desktop') uses: slackapi/slack-github-action@37ebaef184d7626c5f204ab8d3baff4262dd30f0 # v1.27.0 with: channel-id: C074F5UESQ0 @@ -1350,7 +1350,10 @@ jobs: - crowdin-push steps: - name: Check if any job failed - if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure') + if: | + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-desktop') + && contains(needs.*.result, 'failure') run: exit 1 - name: Login to Azure - Prod Subscription diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index bb6909f9864..ba4f2599f37 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -44,6 +44,8 @@ jobs: setup: name: Setup runs-on: ubuntu-22.04 + needs: + - check-run outputs: version: ${{ steps.version.outputs.value }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} @@ -67,7 +69,8 @@ jobs: build-artifacts: name: Build artifacts runs-on: ubuntu-22.04 - needs: setup + needs: + - setup env: _VERSION: ${{ needs.setup.outputs.version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} @@ -151,7 +154,6 @@ jobs: needs: - setup - build-artifacts - - check-run strategy: fail-fast: false matrix: @@ -261,10 +263,9 @@ jobs: crowdin-push: name: Crowdin Push - if: github.ref == 'refs/heads/main' + if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' needs: - build-artifacts - - check-run runs-on: ubuntu-22.04 steps: - name: Check out repo @@ -302,7 +303,6 @@ jobs: runs-on: ubuntu-22.04 needs: - build-artifacts - - check-run steps: - name: Login to Azure - CI Subscription uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -344,7 +344,10 @@ jobs: - trigger-web-vault-deploy steps: - name: Check if any job failed - if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure') + if: | + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-web') + && contains(needs.*.result, 'failure') run: exit 1 - name: Login to Azure - Prod Subscription diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 0686063d561..df664cf8d36 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -2878,7 +2878,7 @@ "message": "E-Mail generieren" }, "generatorBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$", + "message": "Wert muss zwischen $MIN$ und $MAX$ liegen", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 92fabdae3ee..a62dac05430 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -152,6 +152,15 @@ "copyLicenseNumber": { "message": "Copy license number" }, + "copyPrivateKey": { + "message": "Copy private key" + }, + "copyPublicKey": { + "message": "Copy public key" + }, + "copyFingerprint": { + "message": "Copy fingerprint" + }, "copyCustomField": { "message": "Copy $FIELD$", "placeholders": { @@ -1764,6 +1773,9 @@ "typeIdentity": { "message": "Identity" }, + "typeSshKey": { + "message": "SSH key" + }, "newItemHeader": { "message": "New $TYPE$", "placeholders": { @@ -4593,6 +4605,30 @@ "enterprisePolicyRequirementsApplied": { "message": "Enterprise policy requirements have been applied to this setting" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, + "sshKeyAlgorithm": { + "message": "Key type" + }, + "sshKeyAlgorithmED25519": { + "message": "ED25519" + }, + "sshKeyAlgorithmRSA2048": { + "message": "RSA 2048-Bit" + }, + "sshKeyAlgorithmRSA3072": { + "message": "RSA 3072-Bit" + }, + "sshKeyAlgorithmRSA4096": { + "message": "RSA 4096-Bit" + }, "retry": { "message": "Retry" }, diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index cbb1475c4d0..42e56a56ba4 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -20,16 +20,16 @@ "message": "Crear cuenta" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "¿Nuevo en Bitwarden?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Iniciar sesión con clave de acceso" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "Usar inicio de sesión único" }, "welcomeBack": { - "message": "Welcome back" + "message": "Bienvenido de nuevo" }, "setAStrongPassword": { "message": "Establece una contraseña fuerte" @@ -84,7 +84,7 @@ "message": "Incorporarse a la organización" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "Unirse a $ORGANIZATIONNAME$", "placeholders": { "organizationName": { "content": "$1", @@ -120,7 +120,7 @@ "message": "Copiar contraseña" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "Copiar frase de contraseña" }, "copyNote": { "message": "Copiar nota" @@ -153,7 +153,7 @@ "message": "Copiar número de licencia" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "Copiar $FIELD$", "placeholders": { "field": { "content": "$1", @@ -162,13 +162,13 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Copiar sitio web" }, "copyNotes": { - "message": "Copy notes" + "message": "Copiar notas" }, "fill": { - "message": "Fill", + "message": "Rellenar", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -223,13 +223,13 @@ "message": "Añadir elemento" }, "accountEmail": { - "message": "Account email" + "message": "Correo electrónico de la cuenta" }, "requestHint": { - "message": "Request hint" + "message": "Solicitar pista" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "Solicitar pista de la contraseña" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { "message": "Enter your account email address and your password hint will be sent to you" @@ -427,7 +427,7 @@ "message": "Generar contraseña" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "Generar frase de contraseña" }, "regeneratePassword": { "message": "Regenerar contraseña" @@ -567,7 +567,7 @@ "message": "Notas" }, "privateNote": { - "message": "Private note" + "message": "Nota privada" }, "note": { "message": "Nota" @@ -624,7 +624,7 @@ "message": "Tiempo de sesión agotado" }, "vaultTimeoutHeader": { - "message": "Vault timeout" + "message": "Tiempo de espera de la caja fuerte" }, "otherOptions": { "message": "Otras opciones" @@ -645,13 +645,13 @@ "message": "Tu caja fuerte está bloqueada. Verifica tu identidad para continuar." }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "Tu caja fuerte está bloqueada" }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "Tu cuenta está bloqueada" }, "or": { - "message": "or" + "message": "o" }, "unlock": { "message": "Desbloquear" @@ -676,7 +676,7 @@ "message": "Tiempo de espera de la caja fuerte" }, "vaultTimeout1": { - "message": "Timeout" + "message": "Tiempo de espera" }, "lockNow": { "message": "Bloquear" @@ -4708,11 +4708,11 @@ "description": "Represents the - key in screen reader content as a readable word" }, "plusCharacterDescriptor": { - "message": "Plus", + "message": "Más", "description": "Represents the + key in screen reader content as a readable word" }, "equalsCharacterDescriptor": { - "message": "Equals", + "message": "Igual", "description": "Represents the = key in screen reader content as a readable word" }, "braceLeftCharacterDescriptor": { @@ -4736,15 +4736,15 @@ "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Back slash", + "message": "Contrabarra", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { - "message": "Colon", + "message": "Dos puntos", "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "Punto y coma", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 96a10c167a2..054362a4a26 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -1424,7 +1424,7 @@ "message": "Palvelimen URL" }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "Itse ylläpidetyn palvelimen URL-osoite", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -1795,13 +1795,13 @@ "message": "Salasanahistoria" }, "generatorHistory": { - "message": "Generator history" + "message": "Generaattorihistoria" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "Tyhjennä generaattorihistoria" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "Jos jatkat, kaikki generaattorihistorian kohteet poistetaan. Haluatko varmasti jatkaa?" }, "back": { "message": "Takaisin" @@ -1920,7 +1920,7 @@ "message": "Tyhjennä historia" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Mitään näytettävää ei ole" }, "nothingGeneratedRecently": { "message": "Et ole luonut mitään hiljattain" @@ -2710,7 +2710,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { - "message": "out of $TOTAL$", + "message": "/$TOTAL$", "placeholders": { "total": { "content": "$1", diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index c05410956f2..e58a262d59c 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -147,7 +147,7 @@ "message": "Kopiera personnummer" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "Kopiera passnummer" }, "copyLicenseNumber": { "message": "Copy license number" @@ -4624,7 +4624,7 @@ "message": "Items that have been in trash more than 30 days will automatically be deleted" }, "restore": { - "message": "Restore" + "message": "Återställ" }, "deleteForever": { "message": "Delete forever" @@ -4744,7 +4744,7 @@ "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "Semikolon", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { @@ -4756,11 +4756,11 @@ "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Less than", + "message": "Mindre än", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { - "message": "Greater than", + "message": "Större än", "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { @@ -4772,7 +4772,7 @@ "description": "Represents the . key in screen reader content as a readable word" }, "questionCharacterDescriptor": { - "message": "Question mark", + "message": "Frågetecken", "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 76d25262927..26bbbd0054f 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -168,7 +168,7 @@ "message": "複製備註" }, "fill": { - "message": "Fill", + "message": "填入", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -458,7 +458,7 @@ "description": "deprecated. Use specialCharactersLabel instead." }, "include": { - "message": "Include", + "message": "包含", "description": "Card header for password generator include block" }, "uppercaseDescription": { @@ -730,10 +730,10 @@ "message": "安全" }, "confirmMasterPassword": { - "message": "Confirm master password" + "message": "確認主密碼" }, "masterPassword": { - "message": "Master password" + "message": "主密碼" }, "masterPassImportant": { "message": "Your master password cannot be recovered if you forget it!" @@ -1092,10 +1092,10 @@ "message": "This file export will be password protected and require the file password to decrypt." }, "filePassword": { - "message": "File password" + "message": "檔案密碼" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "此密碼將用於匯出和匯入此檔案" }, "accountRestrictedOptionDescription": { "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." @@ -3542,7 +3542,7 @@ "description": "Screen reader text (aria-label) for new item button in overlay" }, "newLogin": { - "message": "New login", + "message": "新增登入資訊", "description": "Button text to display within inline menu when there are no matching items on a login field" }, "addNewLoginItemAria": { diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 696bdb8b896..e79f6f69a36 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -436,9 +436,7 @@ export default class AutofillService implements AutofillServiceInterface { didAutofill = true; if (!options.skipLastUsed) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.cipherService.updateLastUsedDate(options.cipher.id); + await this.cipherService.updateLastUsedDate(options.cipher.id); } // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts index 585f6067e3d..d1005883651 100644 --- a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts +++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts @@ -24,9 +24,9 @@ import { SendFormConfig, SendFormConfigService, SendFormMode, + SendFormModule, } from "@bitwarden/send-ui"; -import { SendFormModule } from "../../../../../../../libs/tools/send/send-ui/src/send-form/send-form.module"; import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component"; import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component"; diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts index 5febed788e7..8d95acbce9d 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -331,6 +331,8 @@ export class AddEditV2Component implements OnInit { return this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLocaleLowerCase()); case CipherType.SecureNote: return this.i18nService.t(partOne, this.i18nService.t("note").toLocaleLowerCase()); + case CipherType.SshKey: + return this.i18nService.t(partOne, this.i18nService.t("typeSshKey").toLocaleLowerCase()); } } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html index f4444a10aeb..973b1f9f1a4 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html @@ -88,3 +88,27 @@ [cipher]="cipher" > + + + + + + + + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts index a53c4a7c355..00a775024ce 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts @@ -48,5 +48,13 @@ export class ItemCopyActionsComponent { return !!this.cipher.notes; } + get hasSshKeyValues() { + return ( + !!this.cipher.sshKey.privateKey || + !!this.cipher.sshKey.publicKey || + !!this.cipher.sshKey.keyFingerprint + ); + } + constructor() {} } diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts index f175c55c826..8ce3bcd2b60 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts @@ -1,7 +1,8 @@ import { CommonModule } from "@angular/common"; import { booleanAttribute, Component, Input, OnInit } from "@angular/core"; import { Router, RouterModule } from "@angular/router"; -import { firstValueFrom, map, Observable } from "rxjs"; +import { BehaviorSubject, firstValueFrom, map, switchMap } from "rxjs"; +import { filter } from "rxjs/operators"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -30,10 +31,18 @@ import { AddEditQueryParams } from "../add-edit/add-edit-v2.component"; imports: [ItemModule, IconButtonModule, MenuModule, CommonModule, JslibModule, RouterModule], }) export class ItemMoreOptionsComponent implements OnInit { + private _cipher$ = new BehaviorSubject(undefined); + @Input({ required: true, }) - cipher: CipherView; + set cipher(c: CipherView) { + this._cipher$.next(c); + } + + get cipher() { + return this._cipher$.value; + } /** * Flag to hide the autofill menu options. Used for items that are @@ -43,7 +52,15 @@ export class ItemMoreOptionsComponent implements OnInit { hideAutofillOptions: boolean; protected autofillAllowed$ = this.vaultPopupAutofillService.autofillAllowed$; - protected canClone$: Observable; + + /** + * Observable that emits a boolean value indicating if the user is authorized to clone the cipher. + * @protected + */ + protected canClone$ = this._cipher$.pipe( + filter((c) => c != null), + switchMap((c) => this.cipherAuthorizationService.canCloneCipher$(c)), + ); /** Boolean dependent on the current user having access to an organization */ protected hasOrganizations = false; @@ -63,7 +80,6 @@ export class ItemMoreOptionsComponent implements OnInit { async ngOnInit(): Promise { this.hasOrganizations = await this.organizationService.hasOrganizations(); - this.canClone$ = this.cipherAuthorizationService.canCloneCipher$(this.cipher); } get canEdit() { diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index cc2c419c04c..b1cbe8bc3e4 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -131,6 +131,8 @@ export class ViewV2Component { ); case CipherType.SecureNote: return this.i18nService.t("viewItemHeader", this.i18nService.t("note").toLowerCase()); + case CipherType.SshKey: + return this.i18nService.t("viewItemHeader", this.i18nService.t("typeSshkey").toLowerCase()); } } diff --git a/apps/browser/src/vault/popup/components/vault/add-edit.component.html b/apps/browser/src/vault/popup/components/vault/add-edit.component.html index 32d7c283b28..fb1efbbbd79 100644 --- a/apps/browser/src/vault/popup/components/vault/add-edit.component.html +++ b/apps/browser/src/vault/popup/components/vault/add-edit.component.html @@ -529,6 +529,26 @@ /> + + +
+
+ {{ "sshPrivateKey" | i18n }} + {{ cipher.sshKey.privateKey }} +
+
+ {{ "sshPublicKey" | i18n }} + {{ cipher.sshKey.publicKey }} +
+
+ {{ "sshKeyFingerprint" | i18n }} + {{ cipher.sshKey.keyFingerprint }} +
+
diff --git a/apps/browser/src/vault/popup/components/vault/vault-filter.component.html b/apps/browser/src/vault/popup/components/vault/vault-filter.component.html index f9ae340a89b..f5c28b2bebd 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-filter.component.html +++ b/apps/browser/src/vault/popup/components/vault/vault-filter.component.html @@ -114,6 +114,19 @@ {{ typeCounts.get(cipherType.SecureNote) || 0 }} +
diff --git a/apps/browser/src/vault/popup/components/vault/vault-items.component.ts b/apps/browser/src/vault/popup/components/vault/vault-items.component.ts index ee6858fe441..27d36cbc2f1 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-items.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-items.component.ts @@ -106,6 +106,9 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn case CipherType.SecureNote: this.groupingTitle = this.i18nService.t("secureNotes"); break; + case CipherType.SshKey: + this.groupingTitle = this.i18nService.t("sshKeys"); + break; default: break; } diff --git a/apps/browser/src/vault/popup/components/vault/view.component.html b/apps/browser/src/vault/popup/components/vault/view.component.html index 73415c9070a..57a5d007d8a 100644 --- a/apps/browser/src/vault/popup/components/vault/view.component.html +++ b/apps/browser/src/vault/popup/components/vault/view.component.html @@ -429,6 +429,39 @@
{{ cipher.identity.country }}
+ +
+
+ + {{ "sshPrivateKey" | i18n }} + +
+
+
+ + {{ "sshPublicKey" | i18n }} + {{ cipher.sshKey.publicKey }} +
+
+ + {{ "sshFingerprint" | i18n }} + {{ cipher.sshKey.keyFingerprint }} +
+
diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts index 42d76e1dfe7..610d48fdc6f 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts @@ -202,6 +202,7 @@ describe("VaultPopupItemsService", () => { [CipherType.Card]: 2, [CipherType.Identity]: 3, [CipherType.SecureNote]: 4, + [CipherType.SshKey]: 5, }; // Assume all ciphers are autofill ciphers to test sorting diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index 09c7d5fb0db..20ac3b3de96 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -277,6 +277,7 @@ export class VaultPopupItemsService { [CipherType.Card]: 2, [CipherType.Identity]: 3, [CipherType.SecureNote]: 4, + [CipherType.SshKey]: 5, }; // Compare types first diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts index 5d7e690193b..02ad7375f6a 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -97,6 +97,7 @@ describe("VaultPopupListFiltersService", () => { CipherType.Card, CipherType.Identity, CipherType.SecureNote, + CipherType.SshKey, ]); }); }); diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts index 4059a43b56e..590807cff60 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -163,6 +163,11 @@ export class VaultPopupListFiltersService { label: this.i18nService.t("note"), icon: "bwi-sticky-note", }, + { + value: CipherType.SshKey, + label: this.i18nService.t("typeSshKey"), + icon: "bwi-key", + }, ]; /** Resets `filterForm` to the original state */ diff --git a/apps/cli/package.json b/apps/cli/package.json index 622c1273823..8ddb5daccd2 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -80,7 +80,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.58", + "tldts": "6.1.60", "zxcvbn": "4.4.2" } } diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index d8a7c470eba..a1ff96ec65d 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -17,6 +17,16 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aes" version = "0.8.4" @@ -28,6 +38,20 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -185,6 +209,28 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-task" version = "4.7.1" @@ -235,12 +281,51 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bcrypt-pbkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2" +dependencies = [ + "blowfish", + "pbkdf2", + "sha2", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "bitwarden-russh" +version = "0.1.0" +source = "git+https://github.com/bitwarden/bitwarden-russh.git?branch=km/pm-10098/clean-russh-implementation#86ff1bf2f4620a3ae5684adee31abdbee33c6f07" +dependencies = [ + "anyhow", + "byteorder", + "futures", + "russh-cryptovec", + "ssh-encoding", + "ssh-key", + "thiserror", + "tokio", + "tokio-util", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -281,6 +366,16 @@ dependencies = [ "piper", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "byteorder" version = "1.5.0" @@ -339,6 +434,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "cipher" version = "0.4.4" @@ -377,6 +483,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "convert_case" version = "0.6.0" @@ -388,9 +500,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.4" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" dependencies = [ "core-foundation-sys", "libc", @@ -437,6 +549,41 @@ dependencies = [ "syn", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "cxx" version = "1.0.129" @@ -481,6 +628,17 @@ dependencies = [ "syn", ] +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.3.11" @@ -508,25 +666,38 @@ dependencies = [ "aes", "anyhow", "arboard", + "async-stream", "base64", + "bitwarden-russh", + "byteorder", "cbc", "core-foundation", "dirs", + "ed25519", "futures", "gio", + "homedir", "interprocess", "keytar", "libc", "libsecret", "log", + "pin-project", + "pkcs8", "rand", + "rand_chacha", "retry", + "rsa", + "russh-cryptovec", "scopeguard", "security-framework", "security-framework-sys", "sha2", + "ssh-encoding", + "ssh-key", "thiserror", "tokio", + "tokio-stream", "tokio-util", "typenum", "widestring", @@ -540,11 +711,14 @@ name = "desktop_napi" version = "0.0.0" dependencies = [ "anyhow", + "base64", "desktop_core", + "hex", "napi", "napi-build", "napi-derive", "tokio", + "tokio-stream", "tokio-util", "windows-registry", ] @@ -570,7 +744,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -615,6 +791,28 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "sha2", + "subtle", +] + [[package]] name = "embed_plist" version = "1.2.2" @@ -697,6 +895,12 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "fixedbitset" version = "0.4.2" @@ -842,6 +1046,16 @@ dependencies = [ "wasi", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.31.1" @@ -885,7 +1099,7 @@ version = "0.19.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39650279f135469465018daae0ba53357942a5212137515777d5fdca74984a44" dependencies = [ - "bitflags", + "bitflags 2.6.0", "futures-channel", "futures-core", "futures-executor", @@ -965,6 +1179,27 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "homedir" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bed305c13ce3829a09d627f5d43ff738482a09361ae4eb8039993b55fb10e5e" +dependencies = [ + "cfg-if", + "nix 0.26.4", + "widestring", + "windows", +] + [[package]] name = "indexmap" version = "2.6.0" @@ -1028,10 +1263,19 @@ dependencies = [ ] [[package]] -name = "libc" -version = "0.2.159" +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.162" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" [[package]] name = "libloading" @@ -1043,13 +1287,19 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "libredox" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags", + "bitflags 2.6.0", "libc", ] @@ -1116,6 +1366,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -1158,7 +1417,7 @@ version = "2.16.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b" dependencies = [ - "bitflags", + "bitflags 2.6.0", "ctor", "napi-derive", "napi-sys", @@ -1210,13 +1469,26 @@ dependencies = [ "libloading", ] +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", + "pin-utils", +] + [[package]] name = "nix" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags", + "bitflags 2.6.0", "cfg-if", "cfg_aliases 0.1.1", "libc", @@ -1228,11 +1500,11 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags", + "bitflags 2.6.0", "cfg-if", "cfg_aliases 0.2.1", "libc", - "memoffset", + "memoffset 0.9.1", ] [[package]] @@ -1245,12 +1517,59 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + [[package]] name = "num_threads" version = "0.1.7" @@ -1282,7 +1601,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags", + "bitflags 2.6.0", "block2", "libc", "objc2", @@ -1298,7 +1617,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags", + "bitflags 2.6.0", "block2", "objc2", "objc2-foundation", @@ -1328,7 +1647,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags", + "bitflags 2.6.0", "block2", "libc", "objc2", @@ -1340,7 +1659,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags", + "bitflags 2.6.0", "block2", "objc2", "objc2-foundation", @@ -1352,7 +1671,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags", + "bitflags 2.6.0", "block2", "objc2", "objc2-foundation", @@ -1374,6 +1693,12 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "option-ext" version = "0.2.0" @@ -1429,6 +1754,25 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "petgraph" version = "0.6.5" @@ -1439,6 +1783,26 @@ dependencies = [ "indexmap", ] +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.15" @@ -1462,6 +1826,44 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes", + "cbc", + "der", + "pbkdf2", + "scrypt", + "sha2", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "pkcs5", + "rand_core", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.31" @@ -1483,6 +1885,29 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1576,7 +2001,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -1628,25 +2053,74 @@ dependencies = [ "rand", ] +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "sha2", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "russh-cryptovec" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fadd2c0ab350e21c66556f94ee06f766d8bdae3213857ba7610bfd8e10e51880" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ - "bitflags", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -1666,12 +2140,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" [[package]] -name = "security-framework" -version = "2.11.0" +name = "scrypt" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" dependencies = [ - "bitflags", + "pbkdf2", + "salsa20", + "sha2", +] + +[[package]] +name = "security-framework" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d0283c0a4a22a0f1b0e4edca251aa20b92fc96eaa09b84bec052f9415e9d71" +dependencies = [ + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -1680,9 +2165,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" dependencies = [ "core-foundation-sys", "libc", @@ -1771,6 +2256,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "simplelog" version = "0.12.2" @@ -1807,12 +2302,81 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "ssh-cipher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" +dependencies = [ + "aes", + "aes-gcm", + "cbc", + "chacha20", + "cipher", + "ctr", + "poly1305", + "ssh-encoding", + "subtle", +] + +[[package]] +name = "ssh-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" +dependencies = [ + "base64ct", + "pem-rfc7468", + "sha2", +] + +[[package]] +name = "ssh-key" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca9b366a80cf18bb6406f4cf4d10aebfb46140a8c0c33f666a144c5c76ecbafc" +dependencies = [ + "bcrypt-pbkdf", + "ed25519-dalek", + "num-bigint-dig", + "rand_core", + "rsa", + "sha2", + "signature", + "ssh-cipher", + "ssh-encoding", + "subtle", + "zeroize", +] + [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.87" @@ -1920,9 +2484,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.41.0" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", @@ -1945,6 +2509,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.12" @@ -2048,7 +2623,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" dependencies = [ - "memoffset", + "memoffset 0.9.1", "tempfile", "winapi", ] @@ -2071,6 +2646,16 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "version-compare" version = "0.2.0" @@ -2109,7 +2694,7 @@ version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" dependencies = [ - "bitflags", + "bitflags 2.6.0", "rustix", "wayland-backend", "wayland-scanner", @@ -2121,7 +2706,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" dependencies = [ - "bitflags", + "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -2133,7 +2718,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" dependencies = [ - "bitflags", + "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -2581,6 +3166,12 @@ dependencies = [ "syn", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zvariant" version = "4.2.0" diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index a56c1b57434..7ed708fc577 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -27,21 +27,39 @@ anyhow = "=1.0.93" arboard = { version = "=3.4.1", default-features = false, features = [ "wayland-data-control", ] } +async-stream = "0.3.5" base64 = "=0.22.1" +byteorder = "1.5.0" cbc = { version = "=0.1.2", features = ["alloc"] } +homedir = "0.3.3" +libc = "=0.2.162" +pin-project = "1.1.5" dirs = "=5.0.1" futures = "=0.3.31" interprocess = { version = "=2.2.1", features = ["tokio"] } -libc = "=0.2.159" log = "=0.4.22" rand = "=0.8.5" retry = "=2.0.0" +russh-cryptovec = "0.7.3" scopeguard = "=1.2.0" sha2 = "=0.10.8" -thiserror = "=1.0.68" -tokio = { version = "=1.41.0", features = ["io-util", "sync", "macros"] } +ssh-encoding = "0.2.0" +ssh-key = { version = "0.6.6", default-features = false, features = [ + "encryption", + "ed25519", + "rsa", + "getrandom", +] } +bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", branch = "km/pm-10098/clean-russh-implementation" } +tokio = { version = "=1.40.0", features = ["io-util", "sync", "macros", "net"] } +tokio-stream = { version = "=0.1.15", features = ["net"] } tokio-util = "=0.7.12" +thiserror = "=1.0.68" typenum = "=1.17.0" +rand_chacha = "=0.3.1" +pkcs8 = { version = "=0.10.2", features = ["alloc", "encryption", "pem"] } +rsa = "=0.9.6" +ed25519 = { version = "=2.2.3", features = ["pkcs8"] } [target.'cfg(windows)'.dependencies] widestring = { version = "=1.1.0", optional = true } @@ -61,9 +79,9 @@ windows = { version = "=0.57.0", features = [ keytar = "=0.1.6" [target.'cfg(target_os = "macos")'.dependencies] -core-foundation = { version = "=0.9.4", optional = true } -security-framework = { version = "=2.11.0", optional = true } -security-framework-sys = { version = "=2.11.0", optional = true } +core-foundation = { version = "=0.10.0", optional = true } +security-framework = { version = "=3.0.0", optional = true } +security-framework-sys = { version = "=2.12.0", optional = true } [target.'cfg(target_os = "linux")'.dependencies] gio = { version = "=0.19.5", optional = true } diff --git a/apps/desktop/desktop_native/core/src/biometric/mod.rs b/apps/desktop/desktop_native/core/src/biometric/mod.rs index c41ad9dda53..72352cf2288 100644 --- a/apps/desktop/desktop_native/core/src/biometric/mod.rs +++ b/apps/desktop/desktop_native/core/src/biometric/mod.rs @@ -6,8 +6,8 @@ use anyhow::{anyhow, Result}; #[cfg_attr(target_os = "macos", path = "macos.rs")] mod biometric; -pub use biometric::Biometric; use base64::{engine::general_purpose::STANDARD as base64_engine, Engine}; +pub use biometric::Biometric; use sha2::{Digest, Sha256}; use crate::crypto::{self, CipherString}; @@ -42,7 +42,6 @@ pub trait BiometricTrait { ) -> Result; } - fn encrypt(secret: &str, key_material: &KeyMaterial, iv_b64: &str) -> Result { let iv = base64_engine .decode(iv_b64)? @@ -77,4 +76,4 @@ impl KeyMaterial { pub fn derive_key(&self) -> Result> { Ok(Sha256::digest(self.digest_material())) } -} \ No newline at end of file +} diff --git a/apps/desktop/desktop_native/core/src/biometric/unix.rs b/apps/desktop/desktop_native/core/src/biometric/unix.rs index 742b736e812..563bd1dfe52 100644 --- a/apps/desktop/desktop_native/core/src/biometric/unix.rs +++ b/apps/desktop/desktop_native/core/src/biometric/unix.rs @@ -5,13 +5,13 @@ use base64::Engine; use rand::RngCore; use sha2::{Digest, Sha256}; -use crate::biometric::{KeyMaterial, OsDerivedKey, base64_engine}; +use crate::biometric::{base64_engine, KeyMaterial, OsDerivedKey}; use zbus::Connection; use zbus_polkit::policykit1::*; use super::{decrypt, encrypt}; -use anyhow::anyhow; use crate::crypto::CipherString; +use anyhow::anyhow; /// The Unix implementation of the biometric trait. pub struct Biometric {} @@ -22,13 +22,15 @@ impl super::BiometricTrait for Biometric { let proxy = AuthorityProxy::new(&connection).await?; let subject = Subject::new_for_owner(std::process::id(), None, None)?; let details = std::collections::HashMap::new(); - let result = proxy.check_authorization( - &subject, - "com.bitwarden.Bitwarden.unlock", - &details, - CheckAuthorizationFlags::AllowUserInteraction.into(), - "", - ).await; + let result = proxy + .check_authorization( + &subject, + "com.bitwarden.Bitwarden.unlock", + &details, + CheckAuthorizationFlags::AllowUserInteraction.into(), + "", + ) + .await; match result { Ok(result) => { @@ -106,4 +108,4 @@ fn random_challenge() -> [u8; 16] { let mut challenge = [0u8; 16]; rand::thread_rng().fill_bytes(&mut challenge); challenge -} \ No newline at end of file +} diff --git a/apps/desktop/desktop_native/core/src/biometric/windows.rs b/apps/desktop/desktop_native/core/src/biometric/windows.rs index c5db9e3277b..d5e8b6dc915 100644 --- a/apps/desktop/desktop_native/core/src/biometric/windows.rs +++ b/apps/desktop/desktop_native/core/src/biometric/windows.rs @@ -160,7 +160,6 @@ impl super::BiometricTrait for Biometric { } } - fn random_challenge() -> [u8; 16] { let mut challenge = [0u8; 16]; rand::thread_rng().fill_bytes(&mut challenge); diff --git a/apps/desktop/desktop_native/core/src/lib.rs b/apps/desktop/desktop_native/core/src/lib.rs index 3132c56f7f8..f38e6ef97b4 100644 --- a/apps/desktop/desktop_native/core/src/lib.rs +++ b/apps/desktop/desktop_native/core/src/lib.rs @@ -11,3 +11,6 @@ pub mod password; pub mod process_isolation; #[cfg(feature = "sys")] pub mod powermonitor; +#[cfg(feature = "sys")] + +pub mod ssh_agent; diff --git a/apps/desktop/desktop_native/core/src/password/unix.rs b/apps/desktop/desktop_native/core/src/password/unix.rs index 53053ee467e..1817a4d62ee 100644 --- a/apps/desktop/desktop_native/core/src/password/unix.rs +++ b/apps/desktop/desktop_native/core/src/password/unix.rs @@ -41,7 +41,11 @@ pub fn delete_password(service: &str, account: &str) -> Result<()> { } pub fn is_available() -> Result { - let result = password_clear_sync(Some(&get_schema()), build_attributes("bitwardenSecretsAvailabilityTest", "test"), gio::Cancellable::NONE); + let result = password_clear_sync( + Some(&get_schema()), + build_attributes("bitwardenSecretsAvailabilityTest", "test"), + gio::Cancellable::NONE, + ); match result { Ok(_) => Ok(true), Err(_) => { diff --git a/apps/desktop/desktop_native/core/src/powermonitor/linux.rs b/apps/desktop/desktop_native/core/src/powermonitor/linux.rs index fe07ad11ff8..7d0fde15ed4 100644 --- a/apps/desktop/desktop_native/core/src/powermonitor/linux.rs +++ b/apps/desktop/desktop_native/core/src/powermonitor/linux.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use zbus::{Connection, MatchRule, export::futures_util::TryStreamExt}; +use zbus::{export::futures_util::TryStreamExt, Connection, MatchRule}; struct ScreenLock { interface: Cow<'static, str>, path: Cow<'static, str>, @@ -42,7 +42,15 @@ pub async fn on_lock(tx: tokio::sync::mpsc::Sender<()>) -> Result<(), Box bool { let connection = Connection::session().await.unwrap(); for monitor in SCREEN_LOCK_MONITORS { - let res = connection.call_method(Some(monitor.interface.clone()), monitor.path.clone(), Some(monitor.interface.clone()), "GetActive", &()).await; + let res = connection + .call_method( + Some(monitor.interface.clone()), + monitor.path.clone(), + Some(monitor.interface.clone()), + "GetActive", + &(), + ) + .await; if res.is_ok() { return true; } diff --git a/apps/desktop/desktop_native/core/src/process_isolation/linux.rs b/apps/desktop/desktop_native/core/src/process_isolation/linux.rs index ba8734cff7f..dc027e0b546 100644 --- a/apps/desktop/desktop_native/core/src/process_isolation/linux.rs +++ b/apps/desktop/desktop_native/core/src/process_isolation/linux.rs @@ -1,7 +1,7 @@ use anyhow::Result; -use libc::{c_int, self}; #[cfg(target_env = "gnu")] use libc::c_uint; +use libc::{self, c_int}; // RLIMIT_CORE is the maximum size of a core dump file. Setting both to 0 disables core dumps, on crashes // https://github.com/torvalds/linux/blob/1613e604df0cd359cf2a7fbd9be7a0bcfacfabd0/include/uapi/asm-generic/resource.h#L20 @@ -22,7 +22,10 @@ pub fn disable_coredumps() -> Result<()> { }; if unsafe { libc::setrlimit(RLIMIT_CORE, &rlimit) } != 0 { let e = std::io::Error::last_os_error(); - return Err(anyhow::anyhow!("failed to disable core dumping, memory might be persisted to disk on crashes {}", e)) + return Err(anyhow::anyhow!( + "failed to disable core dumping, memory might be persisted to disk on crashes {}", + e + )); } Ok(()) @@ -35,7 +38,7 @@ pub fn is_core_dumping_disabled() -> Result { }; if unsafe { libc::getrlimit(RLIMIT_CORE, &mut rlimit) } != 0 { let e = std::io::Error::last_os_error(); - return Err(anyhow::anyhow!("failed to get core dump limit {}", e)) + return Err(anyhow::anyhow!("failed to get core dump limit {}", e)); } Ok(rlimit.rlim_cur == 0 && rlimit.rlim_max == 0) @@ -44,7 +47,10 @@ pub fn is_core_dumping_disabled() -> Result { pub fn disable_memory_access() -> Result<()> { if unsafe { libc::prctl(PR_SET_DUMPABLE, 0) } != 0 { let e = std::io::Error::last_os_error(); - return Err(anyhow::anyhow!("failed to disable memory dumping, memory is dumpable by other processes {}", e)) + return Err(anyhow::anyhow!( + "failed to disable memory dumping, memory is dumpable by other processes {}", + e + )); } Ok(()) diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/generator.rs b/apps/desktop/desktop_native/core/src/ssh_agent/generator.rs new file mode 100644 index 00000000000..fe639f20e7f --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/generator.rs @@ -0,0 +1,45 @@ +use rand::SeedableRng; +use rand_chacha::ChaCha8Rng; +use ssh_key::{Algorithm, HashAlg, LineEnding}; + +use super::importer::SshKey; + +pub async fn generate_keypair(key_algorithm: String) -> Result { + // sourced from cryptographically secure entropy source, with sources for all targets: https://docs.rs/getrandom + // if it cannot be securely sourced, this will panic instead of leading to a weak key + let mut rng: ChaCha8Rng = ChaCha8Rng::from_entropy(); + + let key = match key_algorithm.as_str() { + "ed25519" => ssh_key::PrivateKey::random(&mut rng, Algorithm::Ed25519), + "rsa2048" | "rsa3072" | "rsa4096" => { + let bits = match key_algorithm.as_str() { + "rsa2048" => 2048, + "rsa3072" => 3072, + "rsa4096" => 4096, + _ => return Err(anyhow::anyhow!("Unsupported RSA key size")), + }; + let rsa_keypair = ssh_key::private::RsaKeypair::random(&mut rng, bits) + .or_else(|e| Err(anyhow::anyhow!(e.to_string())))?; + + let private_key = ssh_key::PrivateKey::new( + ssh_key::private::KeypairData::from(rsa_keypair), + "".to_string(), + ) + .or_else(|e| Err(anyhow::anyhow!(e.to_string())))?; + Ok(private_key) + } + _ => { + return Err(anyhow::anyhow!("Unsupported key algorithm")); + } + } + .or_else(|e| Err(anyhow::anyhow!(e.to_string())))?; + + let private_key_openssh = key + .to_openssh(LineEnding::LF) + .or_else(|e| Err(anyhow::anyhow!(e.to_string())))?; + Ok(SshKey { + private_key: private_key_openssh.to_string(), + public_key: key.public_key().to_string(), + key_fingerprint: key.fingerprint(HashAlg::Sha256).to_string(), + }) +} diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/importer.rs b/apps/desktop/desktop_native/core/src/ssh_agent/importer.rs new file mode 100644 index 00000000000..3d643e764c7 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/importer.rs @@ -0,0 +1,395 @@ +use ed25519; +use pkcs8::{ + der::Decode, EncryptedPrivateKeyInfo, ObjectIdentifier, PrivateKeyInfo, SecretDocument, +}; +use ssh_key::{ + private::{Ed25519Keypair, Ed25519PrivateKey, RsaKeypair}, + HashAlg, LineEnding, +}; + +const PKCS1_HEADER: &str = "-----BEGIN RSA PRIVATE KEY-----"; +const PKCS8_UNENCRYPTED_HEADER: &str = "-----BEGIN PRIVATE KEY-----"; +const PKCS8_ENCRYPTED_HEADER: &str = "-----BEGIN ENCRYPTED PRIVATE KEY-----"; +const OPENSSH_HEADER: &str = "-----BEGIN OPENSSH PRIVATE KEY-----"; + +pub const RSA_PKCS8_ALGORITHM_OID: ObjectIdentifier = + ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1"); + +#[derive(Debug)] +enum KeyType { + Ed25519, + Rsa, + Unknown, +} + +pub fn import_key( + encoded_key: String, + password: String, +) -> Result { + match encoded_key.lines().next() { + Some(PKCS1_HEADER) => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::UnsupportedKeyType, + ssh_key: None, + }); + } + Some(PKCS8_UNENCRYPTED_HEADER) => { + return match import_pkcs8_key(encoded_key, None) { + Ok(result) => Ok(result), + Err(_) => Ok(SshKeyImportResult { + status: SshKeyImportStatus::ParsingError, + ssh_key: None, + }), + }; + } + Some(PKCS8_ENCRYPTED_HEADER) => match import_pkcs8_key(encoded_key, Some(password)) { + Ok(result) => { + return Ok(result); + } + Err(err) => match err { + SshKeyImportError::PasswordRequired => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::PasswordRequired, + ssh_key: None, + }); + } + SshKeyImportError::WrongPassword => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::WrongPassword, + ssh_key: None, + }); + } + SshKeyImportError::ParsingError => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::ParsingError, + ssh_key: None, + }); + } + }, + }, + Some(OPENSSH_HEADER) => { + return import_openssh_key(encoded_key, password); + } + Some(_) => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::ParsingError, + ssh_key: None, + }); + } + None => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::ParsingError, + ssh_key: None, + }); + } + } +} + +fn import_pkcs8_key( + encoded_key: String, + password: Option, +) -> Result { + let der = match SecretDocument::from_pem(&encoded_key) { + Ok((_, doc)) => doc, + Err(_) => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::ParsingError, + ssh_key: None, + }); + } + }; + + let decrypted_der = match password.clone() { + Some(password) => { + let encrypted_private_key_info = match EncryptedPrivateKeyInfo::from_der(der.as_bytes()) + { + Ok(info) => info, + Err(_) => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::ParsingError, + ssh_key: None, + }); + } + }; + match encrypted_private_key_info.decrypt(password.as_bytes()) { + Ok(der) => der, + Err(_) => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::WrongPassword, + ssh_key: None, + }); + } + } + } + None => der, + }; + + let key_type: KeyType = match PrivateKeyInfo::from_der(decrypted_der.as_bytes()) + .map_err(|_| SshKeyImportError::ParsingError)? + .algorithm + .oid + { + ed25519::pkcs8::ALGORITHM_OID => KeyType::Ed25519, + RSA_PKCS8_ALGORITHM_OID => KeyType::Rsa, + _ => KeyType::Unknown, + }; + + match key_type { + KeyType::Ed25519 => { + let pk: ed25519::KeypairBytes = match password { + Some(password) => { + pkcs8::DecodePrivateKey::from_pkcs8_encrypted_pem(&encoded_key, password) + .map_err(|err| match err { + ed25519::pkcs8::Error::EncryptedPrivateKey(_) => { + SshKeyImportError::WrongPassword + } + _ => SshKeyImportError::ParsingError, + })? + } + None => ed25519::pkcs8::DecodePrivateKey::from_pkcs8_pem(&encoded_key) + .map_err(|_| SshKeyImportError::ParsingError)?, + }; + let pk: Ed25519Keypair = + Ed25519Keypair::from(Ed25519PrivateKey::from_bytes(&pk.secret_key)); + let private_key = ssh_key::private::PrivateKey::from(pk); + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::Success, + ssh_key: Some(SshKey { + private_key: private_key.to_openssh(LineEnding::LF).unwrap().to_string(), + public_key: private_key.public_key().to_string(), + key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(), + }), + }); + } + KeyType::Rsa => { + let pk: rsa::RsaPrivateKey = match password { + Some(password) => { + pkcs8::DecodePrivateKey::from_pkcs8_encrypted_pem(&encoded_key, password) + .map_err(|err| match err { + pkcs8::Error::EncryptedPrivateKey(_) => { + SshKeyImportError::WrongPassword + } + _ => SshKeyImportError::ParsingError, + })? + } + None => pkcs8::DecodePrivateKey::from_pkcs8_pem(&encoded_key) + .map_err(|_| SshKeyImportError::ParsingError)?, + }; + let rsa_keypair: Result = RsaKeypair::try_from(pk); + match rsa_keypair { + Ok(rsa_keypair) => { + let private_key = ssh_key::private::PrivateKey::from(rsa_keypair); + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::Success, + ssh_key: Some(SshKey { + private_key: private_key + .to_openssh(LineEnding::LF) + .unwrap() + .to_string(), + public_key: private_key.public_key().to_string(), + key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(), + }), + }); + } + Err(_) => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::ParsingError, + ssh_key: None, + }); + } + } + } + _ => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::UnsupportedKeyType, + ssh_key: None, + }); + } + } +} + +fn import_openssh_key( + encoded_key: String, + password: String, +) -> Result { + let private_key = ssh_key::private::PrivateKey::from_openssh(&encoded_key); + let private_key = match private_key { + Ok(k) => k, + Err(err) => { + match err { + ssh_key::Error::AlgorithmUnknown + | ssh_key::Error::AlgorithmUnsupported { algorithm: _ } => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::UnsupportedKeyType, + ssh_key: None, + }); + } + _ => {} + } + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::ParsingError, + ssh_key: None, + }); + } + }; + + if private_key.is_encrypted() && password.is_empty() { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::PasswordRequired, + ssh_key: None, + }); + } + let private_key = if private_key.is_encrypted() { + match private_key.decrypt(password.as_bytes()) { + Ok(k) => k, + Err(_) => { + return Ok(SshKeyImportResult { + status: SshKeyImportStatus::WrongPassword, + ssh_key: None, + }); + } + } + } else { + private_key + }; + + match private_key.to_openssh(LineEnding::LF) { + Ok(private_key_openssh) => Ok(SshKeyImportResult { + status: SshKeyImportStatus::Success, + ssh_key: Some(SshKey { + private_key: private_key_openssh.to_string(), + public_key: private_key.public_key().to_string(), + key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(), + }), + }), + Err(_) => Ok(SshKeyImportResult { + status: SshKeyImportStatus::ParsingError, + ssh_key: None, + }), + } +} + +#[derive(PartialEq, Debug)] +pub enum SshKeyImportStatus { + /// ssh key was parsed correctly and will be returned in the result + Success, + /// ssh key was parsed correctly but is encrypted and requires a password + PasswordRequired, + /// ssh key was parsed correctly, and a password was provided when calling the import, but it was incorrect + WrongPassword, + /// ssh key could not be parsed, either due to an incorrect / unsupported format (pkcs#8) or key type (ecdsa), or because the input is not an ssh key + ParsingError, + /// ssh key type is not supported + UnsupportedKeyType, +} + +pub enum SshKeyImportError { + ParsingError, + PasswordRequired, + WrongPassword, +} + +pub struct SshKeyImportResult { + pub status: SshKeyImportStatus, + pub ssh_key: Option, +} + +pub struct SshKey { + pub private_key: String, + pub public_key: String, + pub key_fingerprint: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn import_key_ed25519_openssh_unencrypted() { + let private_key = include_str!("./test_keys/ed25519_openssh_unencrypted"); + let public_key = include_str!("./test_keys/ed25519_openssh_unencrypted.pub").trim(); + let result = import_key(private_key.to_string(), "".to_string()).unwrap(); + assert_eq!(result.status, SshKeyImportStatus::Success); + assert_eq!(result.ssh_key.unwrap().public_key, public_key); + } + + #[test] + fn import_key_ed25519_openssh_encrypted() { + let private_key = include_str!("./test_keys/ed25519_openssh_encrypted"); + let public_key = include_str!("./test_keys/ed25519_openssh_encrypted.pub").trim(); + let result = import_key(private_key.to_string(), "password".to_string()).unwrap(); + assert_eq!(result.status, SshKeyImportStatus::Success); + assert_eq!(result.ssh_key.unwrap().public_key, public_key); + } + + #[test] + fn import_key_rsa_openssh_unencrypted() { + let private_key = include_str!("./test_keys/rsa_openssh_unencrypted"); + let public_key = include_str!("./test_keys/rsa_openssh_unencrypted.pub").trim(); + let result = import_key(private_key.to_string(), "".to_string()).unwrap(); + assert_eq!(result.status, SshKeyImportStatus::Success); + assert_eq!(result.ssh_key.unwrap().public_key, public_key); + } + + #[test] + fn import_key_rsa_openssh_encrypted() { + let private_key = include_str!("./test_keys/rsa_openssh_encrypted"); + let public_key = include_str!("./test_keys/rsa_openssh_encrypted.pub").trim(); + let result = import_key(private_key.to_string(), "password".to_string()).unwrap(); + assert_eq!(result.status, SshKeyImportStatus::Success); + assert_eq!(result.ssh_key.unwrap().public_key, public_key); + } + + #[test] + fn import_key_ed25519_pkcs8_unencrypted() { + let private_key = include_str!("./test_keys/ed25519_pkcs8_unencrypted"); + let public_key = + include_str!("./test_keys/ed25519_pkcs8_unencrypted.pub").replace("testkey", ""); + let public_key = public_key.trim(); + let result = import_key(private_key.to_string(), "".to_string()).unwrap(); + assert_eq!(result.status, SshKeyImportStatus::Success); + assert_eq!(result.ssh_key.unwrap().public_key, public_key); + } + + #[test] + fn import_key_rsa_pkcs8_unencrypted() { + let private_key = include_str!("./test_keys/rsa_pkcs8_unencrypted"); + // for whatever reason pkcs8 + rsa does not include the comment in the public key + let public_key = + include_str!("./test_keys/rsa_pkcs8_unencrypted.pub").replace("testkey", ""); + let public_key = public_key.trim(); + let result = import_key(private_key.to_string(), "".to_string()).unwrap(); + assert_eq!(result.status, SshKeyImportStatus::Success); + assert_eq!(result.ssh_key.unwrap().public_key, public_key); + } + + #[test] + fn import_key_rsa_pkcs8_encrypted() { + let private_key = include_str!("./test_keys/rsa_pkcs8_encrypted"); + let public_key = include_str!("./test_keys/rsa_pkcs8_encrypted.pub").replace("testkey", ""); + let public_key = public_key.trim(); + let result = import_key(private_key.to_string(), "password".to_string()).unwrap(); + assert_eq!(result.status, SshKeyImportStatus::Success); + assert_eq!(result.ssh_key.unwrap().public_key, public_key); + } + + #[test] + fn import_key_ed25519_openssh_encrypted_wrong_password() { + let private_key = include_str!("./test_keys/ed25519_openssh_encrypted"); + let result = import_key(private_key.to_string(), "wrongpassword".to_string()).unwrap(); + assert_eq!(result.status, SshKeyImportStatus::WrongPassword); + } + + #[test] + fn import_non_key_error() { + let result = import_key("not a key".to_string(), "".to_string()).unwrap(); + assert_eq!(result.status, SshKeyImportStatus::ParsingError); + } + + #[test] + fn import_ecdsa_error() { + let private_key = include_str!("./test_keys/ecdsa_openssh_unencrypted"); + let result = import_key(private_key.to_string(), "".to_string()).unwrap(); + assert_eq!(result.status, SshKeyImportStatus::UnsupportedKeyType); + } +} diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs new file mode 100644 index 00000000000..ad0ac837afc --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs @@ -0,0 +1,118 @@ +use std::sync::Arc; + +use tokio::sync::Mutex; +use tokio_util::sync::CancellationToken; + +use bitwarden_russh::ssh_agent::{self, Key}; + +#[cfg_attr(target_os = "windows", path = "windows.rs")] +#[cfg_attr(target_os = "macos", path = "unix.rs")] +#[cfg_attr(target_os = "linux", path = "unix.rs")] +mod platform_ssh_agent; + +pub mod generator; +pub mod importer; + +#[derive(Clone)] +pub struct BitwardenDesktopAgent { + keystore: ssh_agent::KeyStore, + cancellation_token: CancellationToken, + show_ui_request_tx: tokio::sync::mpsc::Sender<(u32, String)>, + get_ui_response_rx: Arc>>, + request_id: Arc>, +} + +impl BitwardenDesktopAgent { + async fn get_request_id(&self) -> u32 { + let mut request_id = self.request_id.lock().await; + *request_id += 1; + *request_id + } +} + +impl ssh_agent::Agent for BitwardenDesktopAgent { + async fn confirm(&self, ssh_key: Key) -> bool { + let request_id = self.get_request_id().await; + + let mut rx_channel = self.get_ui_response_rx.lock().await.resubscribe(); + self.show_ui_request_tx + .send((request_id, ssh_key.cipher_uuid.clone())) + .await + .expect("Should send request to ui"); + while let Ok((id, response)) = rx_channel.recv().await { + if id == request_id { + return response; + } + } + false + } +} + +impl BitwardenDesktopAgent { + pub fn stop(&self) { + self.cancellation_token.cancel(); + self.keystore + .0 + .write() + .expect("RwLock is not poisoned") + .clear(); + } + + pub fn set_keys( + &mut self, + new_keys: Vec<(String, String, String)>, + ) -> Result<(), anyhow::Error> { + let keystore = &mut self.keystore; + keystore.0.write().expect("RwLock is not poisoned").clear(); + + for (key, name, cipher_id) in new_keys.iter() { + match parse_key_safe(&key) { + Ok(private_key) => { + let public_key_bytes = private_key + .public_key() + .to_bytes() + .expect("Cipher private key is always correctly parsed"); + keystore.0.write().expect("RwLock is not poisoned").insert( + public_key_bytes, + Key { + private_key: Some(private_key), + name: name.clone(), + cipher_uuid: cipher_id.clone(), + }, + ); + } + Err(e) => { + eprintln!("[SSH Agent Native Module] Error while parsing key: {}", e); + } + } + } + + Ok(()) + } + + pub fn lock(&mut self) -> Result<(), anyhow::Error> { + let keystore = &mut self.keystore; + keystore + .0 + .write() + .expect("RwLock is not poisoned") + .iter_mut() + .for_each(|(_public_key, key)| { + key.private_key = None; + }); + Ok(()) + } +} + +fn parse_key_safe(pem: &str) -> Result { + match ssh_key::private::PrivateKey::from_openssh(pem) { + Ok(key) => match key.public_key().to_bytes() { + Ok(_) => Ok(key), + Err(e) => Err(anyhow::Error::msg(format!( + "Failed to parse public key: {}", + e + ))), + }, + Err(e) => Err(anyhow::Error::msg(format!("Failed to parse key: {}", e))), + } +} diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs b/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs new file mode 100644 index 00000000000..69399ae7530 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs @@ -0,0 +1,60 @@ +use std::{ + io, + pin::Pin, + task::{Context, Poll}, +}; + +use futures::Stream; +use tokio::{ + net::windows::named_pipe::{NamedPipeServer, ServerOptions}, + select, +}; +use tokio_util::sync::CancellationToken; + +const PIPE_NAME: &str = r"\\.\pipe\openssh-ssh-agent"; + +#[pin_project::pin_project] +pub struct NamedPipeServerStream { + rx: tokio::sync::mpsc::Receiver, +} + +impl NamedPipeServerStream { + pub fn new(cancellation_token: CancellationToken) -> Self { + let (tx, rx) = tokio::sync::mpsc::channel(16); + tokio::spawn(async move { + println!( + "[SSH Agent Native Module] Creating named pipe server on {}", + PIPE_NAME + ); + let mut listener = ServerOptions::new().create(PIPE_NAME).unwrap(); + loop { + println!("[SSH Agent Native Module] Waiting for connection"); + select! { + _ = cancellation_token.cancelled() => { + println!("[SSH Agent Native Module] Cancellation token triggered, stopping named pipe server"); + break; + } + _ = listener.connect() => { + println!("[SSH Agent Native Module] Incoming connection"); + tx.send(listener).await.unwrap(); + listener = ServerOptions::new().create(PIPE_NAME).unwrap(); + } + } + } + }); + Self { rx } + } +} + +impl Stream for NamedPipeServerStream { + type Item = io::Result; + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + let this = self.project(); + + this.rx.poll_recv(cx).map(|v| v.map(Ok)) + } +} diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ecdsa_openssh_unencrypted b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ecdsa_openssh_unencrypted new file mode 100644 index 00000000000..9cf518f8af7 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ecdsa_openssh_unencrypted @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS +1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQRQzzQ8nQEouF1FMSHkPx1nejNCzF7g +Yb8MHXLdBFM0uJkWs0vzgLJkttts2eDv3SHJqIH6qHpkLtEvgMXE5WcaAAAAoOO1BebjtQ +XmAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFDPNDydASi4XUUx +IeQ/HWd6M0LMXuBhvwwdct0EUzS4mRazS/OAsmS222zZ4O/dIcmogfqoemQu0S+AxcTlZx +oAAAAhAKnIXk6H0Hs3HblklaZ6UmEjjdE/0t7EdYixpMmtpJ4eAAAAB3Rlc3RrZXk= +-----END OPENSSH PRIVATE KEY----- diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ecdsa_openssh_unencrypted.pub b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ecdsa_openssh_unencrypted.pub new file mode 100644 index 00000000000..75e08b88b2f --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ecdsa_openssh_unencrypted.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFDPNDydASi4XUUxIeQ/HWd6M0LMXuBhvwwdct0EUzS4mRazS/OAsmS222zZ4O/dIcmogfqoemQu0S+AxcTlZxo= testkey diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_encrypted b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_encrypted new file mode 100644 index 00000000000..d3244a3d945 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_encrypted @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAUTNb0if +fqsoqtfv70CfukAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIHGs3Uw3eyqnFjBI +2eb7Qto4KVc34ZdnBac59Bab54BLAAAAkPA6aovfxQbP6FoOfaRH6u22CxqiUM0bbMpuFf +WETn9FLaBE6LjoHH0ZI5rzNjJaQUNfx0cRcqsIrexw8YINrdVjySmEqrl5hw8gpgy0gGP5 +1Y6vKWdHdrxJCA9YMFOfDs0UhPfpLKZCwm2Sg+Bd8arlI8Gy7y4Jj/60v2bZOLhD2IZQnK +NdJ8xATiIINuTy4g== +-----END OPENSSH PRIVATE KEY----- diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_encrypted.pub b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_encrypted.pub new file mode 100644 index 00000000000..1188fa43f1e --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_encrypted.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHGs3Uw3eyqnFjBI2eb7Qto4KVc34ZdnBac59Bab54BL testkey diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_unencrypted b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_unencrypted new file mode 100644 index 00000000000..08184f3184e --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_unencrypted @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACAyQo22TXXNqvF+L8jUSSNeu8UqrsDjvf9pwIwDC9ML6gAAAJDSHpL60h6S ++gAAAAtzc2gtZWQyNTUxOQAAACAyQo22TXXNqvF+L8jUSSNeu8UqrsDjvf9pwIwDC9ML6g +AAAECLdlFLIJbEiFo/f0ROdXMNZAPHGPNhvbbftaPsUZEjaDJCjbZNdc2q8X4vyNRJI167 +xSquwOO9/2nAjAML0wvqAAAAB3Rlc3RrZXkBAgMEBQY= +-----END OPENSSH PRIVATE KEY----- diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_unencrypted.pub b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_unencrypted.pub new file mode 100644 index 00000000000..5c398822022 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_unencrypted.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDJCjbZNdc2q8X4vyNRJI167xSquwOO9/2nAjAML0wvq testkey diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_pkcs8_unencrypted b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_pkcs8_unencrypted new file mode 100644 index 00000000000..09eb728601e --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_pkcs8_unencrypted @@ -0,0 +1,4 @@ +-----BEGIN PRIVATE KEY----- +MFECAQEwBQYDK2VwBCIEIDY6/OAdDr3PbDss9NsLXK4CxiKUvz5/R9uvjtIzj4Sz +gSEAxsxm1xpZ/4lKIRYm0JrJ5gRZUh7H24/YT/0qGVGzPa0= +-----END PRIVATE KEY----- diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_pkcs8_unencrypted.pub b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_pkcs8_unencrypted.pub new file mode 100644 index 00000000000..40997e18c89 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_pkcs8_unencrypted.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMbMZtcaWf+JSiEWJtCayeYEWVIex9uP2E/9KhlRsz2t diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_encrypted b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_encrypted new file mode 100644 index 00000000000..bb7bbd85cf9 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_encrypted @@ -0,0 +1,39 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +2MmKBw== +-----END OPENSSH PRIVATE KEY----- diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_encrypted.pub b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_encrypted.pub new file mode 100644 index 00000000000..d37f573b686 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_encrypted.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/v18xGP3qzRV9iWqyiuwHZ4GpC4K2NO2/i2Yv5A3/bnal7CmiMh/S78lphgxcWtFkwrwlb321FmdHBv6KOW+EzSiPvmsdkkbpfBXB3Qf2SlhZOZZ7lYeu8KAxL3exvvn8O1GGlUjXGUrFgmC60tHWDBc1Ncmo8a2dwDLmA/sbLa8su2dvYEFmRg1vaytLDpkn8GS7zAxrUl/g0W2RwkPsByduUziQuX90v9WAy7MqOlwBRq6t5o8wdDBVODe0VIXC7N1OS42YUsKF+N0XOnLiJrIIKkXpahMDpKZHeHQAdUQzsJVhKoLJR8DNDTYyhnJoQG7Q6m2gDTca9oAWvsBiNoEwCvwrt7cDNCz/GslH9HXQgfWcVXn8+fuZgvjO3CxUI16Ev33m0jWoOKJcgK/ZLRnk8SEvsJ8NO32MeR/qUb7IN/yUcDmPMI/3ecQsakF2cwNzHkyiGVo//yVTpf+vk8b89L+GXbYU5rtswtc2ZEGsQnUkaoNqS8mHqhWQBUk= testkey diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_unencrypted b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_unencrypted new file mode 100644 index 00000000000..0d2692e14a2 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_unencrypted @@ -0,0 +1,38 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn +NhAAAAAwEAAQAAAYEAtVIe0gnPtD6299/roT7ntZgVe+qIqIMIruJdI2xTanLGhNpBOlzg +WqokbQK+aXATcaB7iQL1SPxIWV2M4jEBQbZuimIgDQvKbJ4TZPKEe1VdsrfuIo+9pDK7cG +Kc+JiWhKjqeTRMj91/qR1fW5IWOUyE1rkwhTNkwJqtYKZLVmd4TXtQsYMMC+I0cz4krfk1 +Yqmaae/gj12h8BvE3Y+Koof4JoLsqPufH+H/bVEayv63RyAQ1/tUv9l+rwJ+svWV4X3zf3 +z40hGF43L/NGl90Vutbn7b9G/RgEdiXyLZciP3XbWbLUM+r7mG9KNuSeoixe5jok15UKqC +XXxVb5IEZ73kaubSfz9JtsqtKG/OjOq6Fbl3Ky7kjvJyGpIvesuSInlpzPXqbLUCLJJfOA +PUZ1wi8uuuRNePzQBMMhq8UtAbB2Dy16d+HlgghzQ00NxtbQMfDZBdApfxm3shIxkUcHzb +DSvriHVaGGoOkmHPAmsdMsMiekuUMe9ljdOhmdTxAAAFgF8XjBxfF4wcAAAAB3NzaC1yc2 +EAAAGBALVSHtIJz7Q+tvff66E+57WYFXvqiKiDCK7iXSNsU2pyxoTaQTpc4FqqJG0Cvmlw +E3Gge4kC9Uj8SFldjOIxAUG2bopiIA0LymyeE2TyhHtVXbK37iKPvaQyu3BinPiYloSo6n +k0TI/df6kdX1uSFjlMhNa5MIUzZMCarWCmS1ZneE17ULGDDAviNHM+JK35NWKpmmnv4I9d +ofAbxN2PiqKH+CaC7Kj7nx/h/21RGsr+t0cgENf7VL/Zfq8CfrL1leF98398+NIRheNy/z +RpfdFbrW5+2/Rv0YBHYl8i2XIj9121my1DPq+5hvSjbknqIsXuY6JNeVCqgl18VW+SBGe9 +5Grm0n8/SbbKrShvzozquhW5dysu5I7ychqSL3rLkiJ5acz16my1AiySXzgD1GdcIvLrrk +TXj80ATDIavFLQGwdg8tenfh5YIIc0NNDcbW0DHw2QXQKX8Zt7ISMZFHB82w0r64h1Whhq +DpJhzwJrHTLDInpLlDHvZY3ToZnU8QAAAAMBAAEAAAGAEL3wpRWtVTf+NnR5QgX4KJsOjs +bI0ABrVpSFo43uxNMss9sgLzagq5ZurxcUBFHKJdF63puEkPTkbEX4SnFaa5of6kylp3a5 +fd55rXY8F9Q5xtT3Wr8ZdFYP2xBr7INQUJb1MXRMBnOeBDw3UBH01d0UHexzB7WHXcZacG +Ria+u5XrQebwmJ3PYJwENSaTLrxDyjSplQy4QKfgxeWNPWaevylIG9vtue5Xd9WXdl6Szs +ONfD3mFxQZagPSIWl0kYIjS3P2ZpLe8+sakRcfci8RjEUP7U+QxqY5VaQScjyX1cSYeQLz +t+/6Tb167aNtQ8CVW3IzM2EEN1BrSbVxFkxWFLxogAHct06Kn87nPn2+PWGWOVCBp9KheO +FszWAJ0Kzjmaga2BpOJcrwjSpGopAb1YPIoRPVepVZlQ4gGwy5gXCFwykT9WTBoJfg0BMQ +r3MSNcoc97eBomIWEa34K0FuQ3rVjMv9ylfyLvDBbRqTJ5zebeOuU+yCQHZUKk8klRAAAA +wAsToNZvYWRsOMTWQom0EW1IHzoL8Cyua+uh72zZi/7enm4yHPJiu2KNgQXfB0GEEjHjbo +9peCW3gZGTV+Ee+cAqwYLlt0SMl/VJNxN3rEG7BAqPZb42Ii2XGjaxzFq0cliUGAdo6UEd +swU8d2I7m9vIZm4nDXzsWOBWgonTKBNyL0DQ6KNOGEyj8W0BTCm7Rzwy7EKzFWbIxr4lSc +vDrJ3t6kOd7jZTF58kRMT0nxR0bf43YzF/3/qSvLYhQm/OOAAAAMEA2F6Yp8SrpQDNDFxh +gi4GeywArrQO9r3EHjnBZi/bacxllSzCGXAvp7m9OKC1VD2wQP2JL1VEIZRUTuGGT6itrm +QpX8OgoxlEJrlC5W0kHumZ3MFGd33W11u37gOilmd6+VfVXBziNG2rFohweAgs8X+Sg5AA +nIfMV6ySXUlvLzMHpGeKRRnnQq9Cwn4rDkVQENLd1i4e2nWFhaPTUwVMR8YuOT766bywr3 +7vG1PQLF7hnf2c/oPHAru+XD9gJWs5AAAAwQDWiB2G23F4Tvq8FiK2mMusSjQzHupl83rm +o3BSNRCvCjaLx6bWhDPSA1edNEF7VuP6rSp+i+UfSORHwOnlgnrvtcJeoDuA72hUeYuqD/ +1C9gghdhKzGTVf/IGTX1tH3rn2Gq9TEyrJs/ITcoOyZprz7VbaD3bP/NEER+m1EHi2TS/3 +SXQEtRm+IIBwba+QLUcsrWdQyIO+1OCXywDrAw50s7tjgr/goHgXTcrSXaKcIEOlPgBZH3 +YPuVuEtRYgX3kAAAAHdGVzdGtleQECAwQ= +-----END OPENSSH PRIVATE KEY----- diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_unencrypted.pub b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_unencrypted.pub new file mode 100644 index 00000000000..9ec8fec5c58 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_unencrypted.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC1Uh7SCc+0Prb33+uhPue1mBV76oiogwiu4l0jbFNqcsaE2kE6XOBaqiRtAr5pcBNxoHuJAvVI/EhZXYziMQFBtm6KYiANC8psnhNk8oR7VV2yt+4ij72kMrtwYpz4mJaEqOp5NEyP3X+pHV9bkhY5TITWuTCFM2TAmq1gpktWZ3hNe1CxgwwL4jRzPiSt+TViqZpp7+CPXaHwG8Tdj4qih/gmguyo+58f4f9tURrK/rdHIBDX+1S/2X6vAn6y9ZXhffN/fPjSEYXjcv80aX3RW61uftv0b9GAR2JfItlyI/ddtZstQz6vuYb0o25J6iLF7mOiTXlQqoJdfFVvkgRnveRq5tJ/P0m2yq0ob86M6roVuXcrLuSO8nIaki96y5IieWnM9epstQIskl84A9RnXCLy665E14/NAEwyGrxS0BsHYPLXp34eWCCHNDTQ3G1tAx8NkF0Cl/GbeyEjGRRwfNsNK+uIdVoYag6SYc8Cax0ywyJ6S5Qx72WN06GZ1PE= testkey diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_encrypted b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_encrypted new file mode 100644 index 00000000000..e84d1f07a31 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_encrypted @@ -0,0 +1,42 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +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 +-----END ENCRYPTED PRIVATE KEY----- diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_encrypted.pub b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_encrypted.pub new file mode 100644 index 00000000000..f3c1b15f0a3 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_encrypted.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCcHkc0xfH4w9aW41S9M/BfancSY4QPc2O4G1cRjFfK8QrLEGDA7NiHtoEML0afcurRXD3NVxuKaAns0w6EoS4CjzXUqVHTLA4SUyuapr8k0Eu2xOpbCwC3jDovhckoKloq7BvE6rC2i5wjSMadtIJKt/dqWI3HLjUMz1BxQJAU/qAbicj1SFZSjA/MubVBzcq93XOvByMtlIFu7wami3FTc37rVkGeUFHtK8ZbvG3n1aaTF79bBgSPuoq5BfcMdGr4WfQyGQzgse4v4hQ8yKYrtE0jo0kf06hEORimwOIU/W5IH1r+/xFs7qGKcPnFSZRIFv5LfMPTo8b+OsBRflosyfUumDEX97GZE7DSQl0EJzNvWeKwl7dQ8RUJTkbph2CjrxY77DFim+165Uj/WRr4uq2qMNhA2xNSD19+TA6AHdpGw4WZd37q2/n+EddlaJEH8MzpgtHNG9MiYh5ScZ+AG0QugflozJcQNc7n8N9Lpu1sRoejV5RhurHg/TYwVK8= testkey diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_unencrypted b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_unencrypted new file mode 100644 index 00000000000..0bfe2bc5067 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_unencrypted @@ -0,0 +1,40 @@ +-----BEGIN PRIVATE KEY----- +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 +QjDEB37RqGZxqyIx8V2ZYwU= +-----END PRIVATE KEY----- diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_unencrypted.pub b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_unencrypted.pub new file mode 100644 index 00000000000..a3e04eed461 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_unencrypted.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCn4+QiJojZ9mgc9KYJIvDWGaz4qFhf0CButg6L8zEoHKwuiN+mqcEciCCOa9BNiJmm8NTTehZvrrglGG59zIbqYtDAHjVn+vtb49xPzIv+M651Yqj08lIbR9tEIHKCq7aH8GlDm8NgG9EzJGjlL7okQym4TH1MHl+s4mUyr/qb2unlZBDixAQsphU8iCLftukWCIkmQg4CSj1Gh3WbBlZ+EX5eW0EXuAw4XsSbBTWV9CHRowVIpYqPvEYSpHsoCjEcd988p19hpiGknA0J4z7JfUlNgyT/1chb8GCTDT+2DCBRApbsIg6TOBVS+PR6emAQ3eZzUW0+3/oRM4ip0ujltQy8uU6gvYIAqx5wXGMThVpZcUgahKiSsVo/s4b84iMe4DG3W8jz4qi6yyNv0VedEzPUZ1lXd1GJFoy9uKNuSTe+1ksicAcluZN6LuNsPHcPxFCzOcmoNnVXEKAXInt+ys//5CDVasroZSAHZnDjUD4oNsLI3VIOnGxgXrkwSH0= testkey diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs b/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs new file mode 100644 index 00000000000..c1a39506660 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs @@ -0,0 +1,77 @@ +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; + +use bitwarden_russh::ssh_agent; +use homedir::my_home; +use tokio::{net::UnixListener, sync::Mutex}; +use tokio_util::sync::CancellationToken; + +use super::BitwardenDesktopAgent; + +impl BitwardenDesktopAgent { + pub async fn start_server( + auth_request_tx: tokio::sync::mpsc::Sender<(u32, String)>, + auth_response_rx: Arc>>, + ) -> Result { + use std::path::PathBuf; + + let agent = BitwardenDesktopAgent { + keystore: ssh_agent::KeyStore(Arc::new(RwLock::new(HashMap::new()))), + cancellation_token: CancellationToken::new(), + show_ui_request_tx: auth_request_tx, + get_ui_response_rx: auth_response_rx, + request_id: Arc::new(tokio::sync::Mutex::new(0)), + }; + let cloned_agent_state = agent.clone(); + tokio::spawn(async move { + let ssh_path = match std::env::var("BITWARDEN_SSH_AUTH_SOCK") { + Ok(path) => path, + Err(_) => { + println!("[SSH Agent Native Module] BITWARDEN_SSH_AUTH_SOCK not set, using default path"); + + let ssh_agent_directory = match my_home() { + Ok(Some(home)) => home, + _ => PathBuf::from("/tmp/"), + }; + ssh_agent_directory + .join(".bitwarden-ssh-agent.sock") + .to_str() + .expect("Path should be valid") + .to_owned() + } + }; + + println!( + "[SSH Agent Native Module] Starting SSH Agent server on {:?}", + ssh_path + ); + let sockname = std::path::Path::new(&ssh_path); + let _ = std::fs::remove_file(sockname); + match UnixListener::bind(sockname) { + Ok(listener) => { + let wrapper = tokio_stream::wrappers::UnixListenerStream::new(listener); + let cloned_keystore = cloned_agent_state.keystore.clone(); + let cloned_cancellation_token = cloned_agent_state.cancellation_token.clone(); + let _ = ssh_agent::serve( + wrapper, + cloned_agent_state, + cloned_keystore, + cloned_cancellation_token, + ) + .await; + println!("[SSH Agent Native Module] SSH Agent server exited"); + } + Err(e) => { + eprintln!( + "[SSH Agent Native Module] Error while starting agent server: {}", + e + ); + } + } + }); + + Ok(agent) + } +} diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs b/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs new file mode 100644 index 00000000000..fd6d9dacb9f --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs @@ -0,0 +1,41 @@ +use bitwarden_russh::ssh_agent; +pub mod named_pipe_listener_stream; + +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; +use tokio::sync::Mutex; +use tokio_util::sync::CancellationToken; + +use super::BitwardenDesktopAgent; + +impl BitwardenDesktopAgent { + pub async fn start_server( + auth_request_tx: tokio::sync::mpsc::Sender<(u32, String)>, + auth_response_rx: Arc>>, + ) -> Result { + let agent_state = BitwardenDesktopAgent { + keystore: ssh_agent::KeyStore(Arc::new(RwLock::new(HashMap::new()))), + show_ui_request_tx: auth_request_tx, + get_ui_response_rx: auth_response_rx, + cancellation_token: CancellationToken::new(), + request_id: Arc::new(tokio::sync::Mutex::new(0)), + }; + let stream = named_pipe_listener_stream::NamedPipeServerStream::new( + agent_state.cancellation_token.clone(), + ); + + let cloned_agent_state = agent_state.clone(); + tokio::spawn(async move { + let _ = ssh_agent::serve( + stream, + cloned_agent_state.clone(), + cloned_agent_state.keystore.clone(), + cloned_agent_state.cancellation_token.clone(), + ) + .await; + }); + Ok(agent_state) + } +} diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index 64ab106e576..bf7701a6566 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -14,12 +14,15 @@ default = [] manual_test = [] [dependencies] +base64 = "=0.22.1" +hex = "=0.4.3" anyhow = "=1.0.93" desktop_core = { path = "../core" } napi = { version = "=2.16.13", features = ["async"] } napi-derive = "=2.16.12" -tokio = { version = "1.38.0" } -tokio-util = "0.7.11" +tokio = { version = "=1.40.0" } +tokio-util = "=0.7.12" +tokio-stream = "=0.1.15" [target.'cfg(windows)'.dependencies] windows-registry = "=0.3.0" diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 8e1c1381b5f..6d1a7b8abbc 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -42,6 +42,41 @@ export declare namespace clipboards { export function read(): Promise export function write(text: string, password: boolean): Promise } +export declare namespace sshagent { + export interface PrivateKey { + privateKey: string + name: string + cipherId: string + } + export interface SshKey { + privateKey: string + publicKey: string + keyFingerprint: string + } + export const enum SshKeyImportStatus { + /** ssh key was parsed correctly and will be returned in the result */ + Success = 0, + /** ssh key was parsed correctly but is encrypted and requires a password */ + PasswordRequired = 1, + /** ssh key was parsed correctly, and a password was provided when calling the import, but it was incorrect */ + WrongPassword = 2, + /** ssh key could not be parsed, either due to an incorrect / unsupported format (pkcs#8) or key type (ecdsa), or because the input is not an ssh key */ + ParsingError = 3, + /** ssh key type is not supported (e.g. ecdsa) */ + UnsupportedKeyType = 4 + } + export interface SshKeyImportResult { + status: SshKeyImportStatus + sshKey?: SshKey + } + export function serve(callback: (err: Error | null, arg: string) => any): Promise + export function stop(agentState: SshAgentState): void + export function setKeys(agentState: SshAgentState, newKeys: Array): void + export function lock(agentState: SshAgentState): void + export function importKey(encodedKey: string, password: string): SshKeyImportResult + export function generateKeypair(keyAlgorithm: string): Promise + export class SshAgentState { } +} export declare namespace processisolations { export function disableCoredumps(): Promise export function isCoreDumpingDisabled(): Promise diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index face07f2f4e..60a8326a8e5 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -54,12 +54,16 @@ pub mod biometrics { hwnd: napi::bindgen_prelude::Buffer, message: String, ) -> napi::Result { - Biometric::prompt(hwnd.into(), message).await.map_err(|e| napi::Error::from_reason(e.to_string())) + Biometric::prompt(hwnd.into(), message) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) } #[napi] pub async fn available() -> napi::Result { - Biometric::available().await.map_err(|e| napi::Error::from_reason(e.to_string())) + Biometric::available() + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) } #[napi] @@ -151,6 +155,199 @@ pub mod clipboards { } } +#[napi] +pub mod sshagent { + use std::sync::Arc; + + use napi::{ + bindgen_prelude::Promise, + threadsafe_function::{ErrorStrategy::CalleeHandled, ThreadsafeFunction}, + }; + use tokio::{self, sync::Mutex}; + + #[napi] + pub struct SshAgentState { + state: desktop_core::ssh_agent::BitwardenDesktopAgent, + } + + #[napi(object)] + pub struct PrivateKey { + pub private_key: String, + pub name: String, + pub cipher_id: String, + } + + #[napi(object)] + pub struct SshKey { + pub private_key: String, + pub public_key: String, + pub key_fingerprint: String, + } + + impl From for SshKey { + fn from(key: desktop_core::ssh_agent::importer::SshKey) -> Self { + SshKey { + private_key: key.private_key, + public_key: key.public_key, + key_fingerprint: key.key_fingerprint, + } + } + } + + #[napi] + pub enum SshKeyImportStatus { + /// ssh key was parsed correctly and will be returned in the result + Success, + /// ssh key was parsed correctly but is encrypted and requires a password + PasswordRequired, + /// ssh key was parsed correctly, and a password was provided when calling the import, but it was incorrect + WrongPassword, + /// ssh key could not be parsed, either due to an incorrect / unsupported format (pkcs#8) or key type (ecdsa), or because the input is not an ssh key + ParsingError, + /// ssh key type is not supported (e.g. ecdsa) + UnsupportedKeyType, + } + + impl From for SshKeyImportStatus { + fn from(status: desktop_core::ssh_agent::importer::SshKeyImportStatus) -> Self { + match status { + desktop_core::ssh_agent::importer::SshKeyImportStatus::Success => { + SshKeyImportStatus::Success + } + desktop_core::ssh_agent::importer::SshKeyImportStatus::PasswordRequired => { + SshKeyImportStatus::PasswordRequired + } + desktop_core::ssh_agent::importer::SshKeyImportStatus::WrongPassword => { + SshKeyImportStatus::WrongPassword + } + desktop_core::ssh_agent::importer::SshKeyImportStatus::ParsingError => { + SshKeyImportStatus::ParsingError + } + desktop_core::ssh_agent::importer::SshKeyImportStatus::UnsupportedKeyType => { + SshKeyImportStatus::UnsupportedKeyType + } + } + } + } + + #[napi(object)] + pub struct SshKeyImportResult { + pub status: SshKeyImportStatus, + pub ssh_key: Option, + } + + impl From for SshKeyImportResult { + fn from(result: desktop_core::ssh_agent::importer::SshKeyImportResult) -> Self { + SshKeyImportResult { + status: result.status.into(), + ssh_key: result.ssh_key.map(|k| k.into()), + } + } + } + + #[napi] + pub async fn serve( + callback: ThreadsafeFunction, + ) -> napi::Result { + let (auth_request_tx, mut auth_request_rx) = tokio::sync::mpsc::channel::<(u32, String)>(32); + let (auth_response_tx, auth_response_rx) = tokio::sync::broadcast::channel::<(u32, bool)>(32); + let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx)); + tokio::spawn(async move { + let _ = auth_response_rx; + + while let Some((request_id, cipher_uuid)) = auth_request_rx.recv().await { + let cloned_request_id = request_id.clone(); + let cloned_cipher_uuid = cipher_uuid.clone(); + let cloned_response_tx_arc = auth_response_tx_arc.clone(); + let cloned_callback = callback.clone(); + tokio::spawn(async move { + let request_id = cloned_request_id; + let cipher_uuid = cloned_cipher_uuid; + let auth_response_tx_arc = cloned_response_tx_arc; + let callback = cloned_callback; + let promise_result: Result, napi::Error> = + callback.call_async(Ok(cipher_uuid)).await; + match promise_result { + Ok(promise_result) => match promise_result.await { + Ok(result) => { + let _ = auth_response_tx_arc.lock().await.send((request_id, result)) + .expect("should be able to send auth response to agent"); + } + Err(e) => { + println!("[SSH Agent Native Module] calling UI callback promise was rejected: {}", e); + let _ = auth_response_tx_arc.lock().await.send((request_id, false)) + .expect("should be able to send auth response to agent"); + } + }, + Err(e) => { + println!("[SSH Agent Native Module] calling UI callback could not create promise: {}", e); + let _ = auth_response_tx_arc.lock().await.send((request_id, false)) + .expect("should be able to send auth response to agent"); + } + } + }); + } + }); + + match desktop_core::ssh_agent::BitwardenDesktopAgent::start_server( + auth_request_tx, + Arc::new(Mutex::new(auth_response_rx)), + ) + .await + { + Ok(state) => Ok(SshAgentState { state }), + Err(e) => Err(napi::Error::from_reason(e.to_string())), + } + } + + #[napi] + pub fn stop(agent_state: &mut SshAgentState) -> napi::Result<()> { + let bitwarden_agent_state = &mut agent_state.state; + bitwarden_agent_state.stop(); + Ok(()) + } + + #[napi] + pub fn set_keys( + agent_state: &mut SshAgentState, + new_keys: Vec, + ) -> napi::Result<()> { + let bitwarden_agent_state = &mut agent_state.state; + bitwarden_agent_state + .set_keys( + new_keys + .iter() + .map(|k| (k.private_key.clone(), k.name.clone(), k.cipher_id.clone())) + .collect(), + ) + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + Ok(()) + } + + #[napi] + pub fn lock(agent_state: &mut SshAgentState) -> napi::Result<()> { + let bitwarden_agent_state = &mut agent_state.state; + bitwarden_agent_state + .lock() + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub fn import_key(encoded_key: String, password: String) -> napi::Result { + let result = desktop_core::ssh_agent::importer::import_key(encoded_key, password) + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + Ok(result.into()) + } + + #[napi] + pub async fn generate_keypair(key_algorithm: String) -> napi::Result { + desktop_core::ssh_agent::generator::generate_keypair(key_algorithm) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + .map(|k| k.into()) + } +} + #[napi] pub mod processisolations { #[napi] @@ -172,12 +369,19 @@ pub mod processisolations { #[napi] pub mod powermonitors { - use napi::{threadsafe_function::{ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode}, tokio}; + use napi::{ + threadsafe_function::{ + ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode, + }, + tokio, + }; #[napi] pub async fn on_lock(callback: ThreadsafeFunction<(), CalleeHandled>) -> napi::Result<()> { let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(32); - desktop_core::powermonitor::on_lock(tx).await.map_err(|e| napi::Error::from_reason(e.to_string()))?; + desktop_core::powermonitor::on_lock(tx) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; tokio::spawn(async move { while let Some(message) = rx.recv().await { callback.call(Ok(message.into()), ThreadsafeFunctionCallMode::NonBlocking); @@ -190,7 +394,6 @@ pub mod powermonitors { pub async fn is_lock_monitor_available() -> napi::Result { Ok(desktop_core::powermonitor::is_lock_monitor_available().await) } - } #[napi] diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index 7e1d7193b58..f57f067907a 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -14,7 +14,7 @@ "module-alias": "2.2.3", "node-ipc": "9.2.1", "ts-node": "10.9.2", - "uuid": "11.0.1", + "uuid": "11.0.3", "yargs": "17.7.2" }, "devDependencies": { @@ -421,9 +421,9 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.1.tgz", - "integrity": "sha512-wt9UB5EcLhnboy1UvA1mvGPXkIIrHSu+3FmUksARfdVw9tuPf3CH/CohxO0Su1ApoKAeT6BVzAJIvjTuQVSmuQ==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index 0c38902ea4c..ed2c4bb29cf 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -19,7 +19,7 @@ "module-alias": "2.2.3", "node-ipc": "9.2.1", "ts-node": "10.9.2", - "uuid": "11.0.1", + "uuid": "11.0.3", "yargs": "17.7.2" }, "devDependencies": { diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index 76cf98b1b24..7336ce09dd8 100644 --- a/apps/desktop/src/app/accounts/settings.component.html +++ b/apps/desktop/src/app/accounts/settings.component.html @@ -419,6 +419,23 @@ "enableHardwareAccelerationDesc" | i18n }}
+
+
+ +
+ {{ + "enableSshAgentDesc" | i18n + }} +
+ +
+
+
+ +
+
+
+
+ + + +
+
+
+
+ + +
+
+ +
+
+
+
+ + +
+
+ +
+
+
+ +
+
diff --git a/apps/desktop/src/vault/app/vault/add-edit.component.ts b/apps/desktop/src/vault/app/vault/add-edit.component.ts index e34d9cbb412..a3a9c929159 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.ts +++ b/apps/desktop/src/vault/app/vault/add-edit.component.ts @@ -1,6 +1,7 @@ import { DatePipe } from "@angular/common"; -import { Component, NgZone, OnChanges, OnInit, OnDestroy, ViewChild } from "@angular/core"; +import { Component, NgZone, OnChanges, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { NgForm } from "@angular/forms"; +import { sshagent as sshAgent } from "desktop_native/napi"; import { CollectionService } from "@bitwarden/admin-console/common"; import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component"; @@ -18,8 +19,9 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; const BroadcasterSubscriptionId = "AddEditComponent"; @@ -31,6 +33,7 @@ const BroadcasterSubscriptionId = "AddEditComponent"; export class AddEditComponent extends BaseAddEditComponent implements OnInit, OnChanges, OnDestroy { @ViewChild("form") private form: NgForm; + constructor( cipherService: CipherService, folderService: FolderService, @@ -51,6 +54,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On dialogService: DialogService, datePipe: DatePipe, configService: ConfigService, + private toastService: ToastService, cipherAuthorizationService: CipherAuthorizationService, ) { super( @@ -140,4 +144,68 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On "https://bitwarden.com/help/managing-items/#protect-individual-items", ); } + + async generateSshKey() { + const sshKey = await ipc.platform.sshAgent.generateKey("ed25519"); + this.cipher.sshKey.privateKey = sshKey.privateKey; + this.cipher.sshKey.publicKey = sshKey.publicKey; + this.cipher.sshKey.keyFingerprint = sshKey.keyFingerprint; + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("sshKeyGenerated"), + }); + } + + async importSshKeyFromClipboard() { + const key = await this.platformUtilsService.readFromClipboard(); + const parsedKey = await ipc.platform.sshAgent.importKey(key, ""); + if (parsedKey == null || parsedKey.status === sshAgent.SshKeyImportStatus.ParsingError) { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t("invalidSshKey"), + }); + return; + } else if (parsedKey.status === sshAgent.SshKeyImportStatus.UnsupportedKeyType) { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t("sshKeyTypeUnsupported"), + }); + } else if ( + parsedKey.status === sshAgent.SshKeyImportStatus.PasswordRequired || + parsedKey.status === sshAgent.SshKeyImportStatus.WrongPassword + ) { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t("sshKeyPasswordUnsupported"), + }); + return; + } else { + this.cipher.sshKey.privateKey = parsedKey.sshKey.privateKey; + this.cipher.sshKey.publicKey = parsedKey.sshKey.publicKey; + this.cipher.sshKey.keyFingerprint = parsedKey.sshKey.keyFingerprint; + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("sshKeyPasted"), + }); + } + } + + async typeChange() { + if (this.cipher.type === CipherType.SshKey) { + await this.generateSshKey(); + } + } + + truncateString(value: string, length: number) { + return value.length > length ? value.substring(0, length) + "..." : value; + } + + togglePrivateKey() { + this.showPrivateKey = !this.showPrivateKey; + } } diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.html b/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.html index 381c06e8b67..c3dcd191dfc 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.html +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.html @@ -79,4 +79,19 @@ +
  • + + + +
  • diff --git a/apps/desktop/src/vault/app/vault/view.component.html b/apps/desktop/src/vault/app/vault/view.component.html index c855224cf9f..e6c20d2e89e 100644 --- a/apps/desktop/src/vault/app/vault/view.component.html +++ b/apps/desktop/src/vault/app/vault/view.component.html @@ -399,6 +399,105 @@
    {{ cipher.identity.country }}
    + +
    +
    +
    + {{ "sshPrivateKey" | i18n }} +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    diff --git a/apps/web/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 b2812727473..c77c8fc935f 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html @@ -262,7 +262,9 @@ {{ "revokeAccess" | i18n }} - + - + (); + protected enableTrialPayment$ = this.configService.getFeatureFlag$( + FeatureFlag.TrialPaymentOptional, + ); + + constructor( + private route: ActivatedRoute, + private configService: ConfigService, + protected formBuilder: UntypedFormBuilder, + protected i18nService: I18nService, + protected organizationBillingService: OrganizationBillingService, + protected router: Router, + ) { + super(formBuilder, i18nService, organizationBillingService, router); + } + + async ngOnInit(): Promise { + this.referenceEventRequest = new ReferenceEventRequest(); + this.referenceEventRequest.initiationPath = "Secrets Manager trial from marketing website"; + + this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((qParams) => { + if (trialFlowOrgs.includes(qParams.org)) { + if (qParams.org === ValidOrgParams.teamsStarter) { + this.plan = PlanType.TeamsStarter; + } else if (qParams.org === ValidOrgParams.teams) { + this.plan = PlanType.TeamsAnnually; + } else if (qParams.org === ValidOrgParams.enterprise) { + this.plan = PlanType.EnterpriseAnnually; + } + } + }); + } + organizationCreated(event: OrganizationCreatedEvent) { this.organizationId = event.organizationId; this.billingSubLabel = event.planDescription; @@ -31,6 +85,29 @@ export class SecretsManagerTrialPaidStepperComponent extends SecretsManagerTrial this.verticalStepper.previous(); } + async createOrganizationOnTrial(): Promise { + this.createOrganizationLoading = true; + const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({ + organization: { + name: this.formGroup.get("name").value, + billingEmail: this.formGroup.get("email").value, + initiationPath: "Secrets Manager trial from marketing website", + }, + plan: { + type: this.plan, + subscribeToSecretsManager: true, + isFromSecretsManagerTrial: true, + passwordManagerSeats: 1, + secretsManagerSeats: 1, + }, + }); + + this.organizationId = response?.id; + this.subLabels.organizationInfo = response?.name; + this.createOrganizationLoading = false; + this.verticalStepper.next(); + } + get createAccountLabel() { const organizationType = this.productType === ProductTierType.TeamsStarter diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html index ed1dc6cda9b..077836a7634 100644 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html +++ b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html @@ -91,12 +91,17 @@ bitButton buttonType="primary" [disabled]="orgInfoFormGroup.get('name').invalid" - cdkStepperNext + [loading]="loading" + (click)="createOrganizationOnTrial()" > - {{ "next" | i18n }} + {{ (enableTrialPayment$ | async) ? ("startTrial" | i18n) : ("next" | i18n) }} - + { let policyServiceMock: MockProxy; let routerServiceMock: MockProxy; let acceptOrgInviteServiceMock: MockProxy; + let organizationBillingServiceMock: MockProxy; + let configServiceMock: MockProxy; beforeEach(() => { // only define services directly that we want to mock return values in this component @@ -47,6 +51,8 @@ describe("TrialInitiationComponent", () => { policyServiceMock = mock(); routerServiceMock = mock(); acceptOrgInviteServiceMock = mock(); + organizationBillingServiceMock = mock(); + configServiceMock = mock(); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -92,6 +98,14 @@ describe("TrialInitiationComponent", () => { provide: AcceptOrganizationInviteService, useValue: acceptOrgInviteServiceMock, }, + { + provide: OrganizationBillingService, + useValue: organizationBillingServiceMock, + }, + { + provide: ConfigService, + useValue: configServiceMock, + }, ], schemas: [NO_ERRORS_SCHEMA], // Allows child components to be ignored (such as register component) }).compileComponents(); diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts index f8718b0a420..7892283a387 100644 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts +++ b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts @@ -9,8 +9,15 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { + OrganizationInformation, + PlanInformation, + OrganizationBillingServiceAbstraction as OrganizationBillingService, +} from "@bitwarden/common/billing/abstractions/organization-billing.service"; import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -25,7 +32,7 @@ import { OrganizationInvite } from "../organization-invite/organization-invite"; import { RouterService } from "./../../core/router.service"; import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component"; -enum ValidOrgParams { +export enum ValidOrgParams { families = "families", enterprise = "enterprise", teams = "teams", @@ -69,6 +76,7 @@ export class TrialInitiationComponent implements OnInit, OnDestroy { productTier: ProductTierType; accountCreateOnly = true; useTrialStepper = false; + loading = false; policies: Policy[]; enforcedPolicyOptions: MasterPasswordPolicyOptions; trialFlowOrgs: string[] = [ @@ -115,6 +123,9 @@ export class TrialInitiationComponent implements OnInit, OnDestroy { } private destroy$ = new Subject(); + protected enableTrialPayment$ = this.configService.getFeatureFlag$( + FeatureFlag.TrialPaymentOptional, + ); constructor( private route: ActivatedRoute, @@ -127,6 +138,8 @@ export class TrialInitiationComponent implements OnInit, OnDestroy { private i18nService: I18nService, private routerService: RouterService, private acceptOrgInviteService: AcceptOrganizationInviteService, + private organizationBillingService: OrganizationBillingService, + private configService: ConfigService, ) {} async ngOnInit(): Promise { @@ -215,6 +228,30 @@ export class TrialInitiationComponent implements OnInit, OnDestroy { } } + async createOrganizationOnTrial() { + this.loading = true; + const organization: OrganizationInformation = { + name: this.orgInfoFormGroup.get("name").value, + billingEmail: this.orgInfoFormGroup.get("email").value, + initiationPath: "Password Manager trial from marketing website", + }; + + const plan: PlanInformation = { + type: this.plan, + passwordManagerSeats: 1, + }; + + const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({ + organization, + plan, + }); + + this.orgId = response?.id; + this.billingSubLabel = `${this.i18nService.t("annual")} ($0/${this.i18nService.t("yr")})`; + this.loading = false; + this.verticalStepper.next(); + } + createdAccount(email: string) { this.email = email; this.orgInfoFormGroup.get("email")?.setValue(email); diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html index e6ed6475c4a..878672a1fb9 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html @@ -345,16 +345,22 @@

    diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts index 0ba4829c7c8..5a6ac8c896a 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts @@ -282,6 +282,12 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { : this.discountPercentageFromSub + this.discountPercentage; } + isPaymentSourceEmpty() { + return this.deprecateStripeSourcesAPI + ? this.paymentSource === null || this.paymentSource === undefined + : this.billing?.paymentSource === null || this.billing?.paymentSource === undefined; + } + isSecretsManagerTrial(): boolean { return ( this.sub?.subscription?.items?.some((item) => @@ -723,7 +729,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { // Secrets Manager this.buildSecretsManagerRequest(request); - if (this.upgradeRequiresPaymentMethod || this.showPayment) { + if (this.upgradeRequiresPaymentMethod || this.showPayment || this.isPaymentSourceEmpty()) { if (this.deprecateStripeSourcesAPI) { const tokenizedPaymentSource = await this.paymentV2Component.tokenize(); const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); diff --git a/apps/web/src/app/billing/organizations/organization-billing.module.ts b/apps/web/src/app/billing/organizations/organization-billing.module.ts index ccfe12b2e59..b25cda662f2 100644 --- a/apps/web/src/app/billing/organizations/organization-billing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from "@angular/core"; +import { BannerModule } from "../../../../../../libs/components/src/banner/banner.module"; import { UserVerificationModule } from "../../auth/shared/components/user-verification"; import { LooseComponentsModule } from "../../shared"; import { BillingSharedModule } from "../shared"; @@ -28,6 +29,7 @@ import { SubscriptionStatusComponent } from "./subscription-status.component"; BillingSharedModule, OrganizationPlansComponent, LooseComponentsModule, + BannerModule, ], declarations: [ AdjustSubscription, diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html index 9f9cb9efc65..7a6e8558bae 100644 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html +++ b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html @@ -1,3 +1,22 @@ + + {{ freeTrialData.message }} + + {{ "routeToPaymentMethodTrigger" | i18n }} + + diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts index 0756a6c314c..e2178e7c02c 100644 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts +++ b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts @@ -1,17 +1,25 @@ -import { Component, ViewChild } from "@angular/core"; +import { Location } from "@angular/common"; +import { Component, OnDestroy, ViewChild } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; import { from, lastValueFrom, switchMap } from "rxjs"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; +import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { DialogService, ToastService } from "@bitwarden/components"; +import { FreeTrial } from "../../../core/types/free-trial"; +import { TrialFlowService } from "../../services/trial-flow.service"; import { TaxInfoComponent } from "../../shared"; import { AddCreditDialogResult, @@ -25,26 +33,36 @@ import { @Component({ templateUrl: "./organization-payment-method.component.html", }) -export class OrganizationPaymentMethodComponent { +export class OrganizationPaymentMethodComponent implements OnDestroy { @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent; organizationId: string; + isUnpaid = false; accountCredit: number; paymentSource?: PaymentSourceResponse; subscriptionStatus?: string; + protected freeTrialData: FreeTrial; + organization: Organization; + organizationSubscriptionResponse: OrganizationSubscriptionResponse; loading = true; protected readonly Math = Math; + launchPaymentModalAutomatically = false; constructor( private activatedRoute: ActivatedRoute, private billingApiService: BillingApiServiceAbstraction, + protected organizationApiService: OrganizationApiServiceAbstraction, private dialogService: DialogService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private router: Router, private toastService: ToastService, + private location: Location, + private trialFlowService: TrialFlowService, + private organizationService: OrganizationService, + protected syncService: SyncService, ) { this.activatedRoute.params .pipe( @@ -59,6 +77,23 @@ export class OrganizationPaymentMethodComponent { }), ) .subscribe(); + + const state = this.router.getCurrentNavigation()?.extras?.state; + // incase the above state is undefined or null we use redundantState + const redundantState: any = location.getState(); + if (state && Object.prototype.hasOwnProperty.call(state, "launchPaymentModalAutomatically")) { + this.launchPaymentModalAutomatically = state.launchPaymentModalAutomatically; + } else if ( + redundantState && + Object.prototype.hasOwnProperty.call(redundantState, "launchPaymentModalAutomatically") + ) { + this.launchPaymentModalAutomatically = redundantState.launchPaymentModalAutomatically; + } else { + this.launchPaymentModalAutomatically = false; + } + } + ngOnDestroy(): void { + this.launchPaymentModalAutomatically = false; } protected addAccountCredit = async (): Promise => { @@ -82,6 +117,34 @@ export class OrganizationPaymentMethodComponent { this.accountCredit = accountCredit; this.paymentSource = paymentSource; this.subscriptionStatus = subscriptionStatus; + + if (this.organizationId) { + const organizationSubscriptionPromise = this.organizationApiService.getSubscription( + this.organizationId, + ); + const organizationPromise = this.organizationService.get(this.organizationId); + + [this.organizationSubscriptionResponse, this.organization] = await Promise.all([ + organizationSubscriptionPromise, + organizationPromise, + ]); + this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( + this.organization, + this.organizationSubscriptionResponse, + paymentSource, + ); + } + this.isUnpaid = this.subscriptionStatus === "unpaid" ?? false; + // If the flag `launchPaymentModalAutomatically` is set to true, + // we schedule a timeout (delay of 800ms) to automatically launch the payment modal. + // This delay ensures that any prior UI/rendering operations complete before triggering the modal. + if (this.launchPaymentModalAutomatically) { + window.setTimeout(async () => { + await this.changePayment(); + this.launchPaymentModalAutomatically = false; + this.location.replaceState(this.location.path(), "", {}); + }, 800); + } this.loading = false; }; @@ -100,6 +163,24 @@ export class OrganizationPaymentMethodComponent { } }; + changePayment = async () => { + const dialogRef = AdjustPaymentDialogV2Component.open(this.dialogService, { + data: { + initialPaymentMethod: this.paymentSource?.type, + organizationId: this.organizationId, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === AdjustPaymentDialogV2ResultType.Submitted) { + this.location.replaceState(this.location.path(), "", {}); + if (this.launchPaymentModalAutomatically && !this.organization.enabled) { + await this.syncService.fullSync(true); + } + this.launchPaymentModalAutomatically = false; + await this.load(); + } + }; + protected updateTaxInformation = async (): Promise => { this.taxInfoComponent.taxFormGroup.updateValueAndValidity(); this.taxInfoComponent.taxFormGroup.markAllAsTouched(); diff --git a/apps/web/src/app/billing/services/trial-flow.service.ts b/apps/web/src/app/billing/services/trial-flow.service.ts new file mode 100644 index 00000000000..3135a811665 --- /dev/null +++ b/apps/web/src/app/billing/services/trial-flow.service.ts @@ -0,0 +1,100 @@ +import { Injectable } from "@angular/core"; +import { Router } from "@angular/router"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; +import { BillingSourceResponse } from "@bitwarden/common/billing/models/response/billing.response"; +import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; +import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; +import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService } from "@bitwarden/components"; + +import { FreeTrial } from "../../core/types/free-trial"; + +@Injectable({ providedIn: "root" }) +export class TrialFlowService { + constructor( + private i18nService: I18nService, + protected dialogService: DialogService, + private router: Router, + protected billingApiService: BillingApiServiceAbstraction, + ) {} + checkForOrgsWithUpcomingPaymentIssues( + organization: Organization, + organizationSubscription: OrganizationSubscriptionResponse, + paymentSource: BillingSourceResponse | PaymentSourceResponse, + ): FreeTrial { + const trialEndDate = organizationSubscription?.subscription?.trialEndDate; + const displayBanner = + !paymentSource && + organization?.isOwner && + organizationSubscription?.subscription?.status === "trialing"; + const trialRemainingDays = trialEndDate ? this.calculateTrialRemainingDays(trialEndDate) : 0; + const freeTrialMessage = this.getFreeTrialMessage(trialRemainingDays); + + return { + remainingDays: trialRemainingDays, + message: freeTrialMessage, + shownBanner: displayBanner, + organizationId: organization.id, + organizationName: organization.name, + }; + } + + calculateTrialRemainingDays(trialEndDate: string): number | undefined { + const today = new Date(); + const trialEnd = new Date(trialEndDate); + const timeDifference = trialEnd.getTime() - today.getTime(); + + return Math.ceil(timeDifference / (1000 * 60 * 60 * 24)); + } + + getFreeTrialMessage(trialRemainingDays: number): string { + if (trialRemainingDays >= 2) { + return this.i18nService.t("freeTrialEndPrompt", trialRemainingDays); + } else if (trialRemainingDays === 1) { + return this.i18nService.t("freeTrialEndPromptForOneDayNoOrgName"); + } else { + return this.i18nService.t("freeTrialEndingSoonWithoutOrgName"); + } + } + + async handleUnpaidSubscriptionDialog( + org: Organization, + organizationBillingMetadata: OrganizationBillingMetadataResponse, + ): Promise { + if (organizationBillingMetadata.isSubscriptionUnpaid) { + const confirmed = await this.promptForPaymentNavigation(org); + if (confirmed) { + await this.navigateToPaymentMethod(org?.id); + } + } + } + + private async promptForPaymentNavigation(org: Organization): Promise { + if (!org?.isOwner) { + await this.dialogService.openSimpleDialog({ + title: this.i18nService.t("suspendedOrganizationTitle", org?.name), + content: { key: "suspendedUserOrgMessage" }, + type: "danger", + acceptButtonText: this.i18nService.t("close"), + cancelButtonText: null, + }); + return false; + } + return await this.dialogService.openSimpleDialog({ + title: this.i18nService.t("suspendedOrganizationTitle", org?.name), + content: { key: "suspendedOwnerOrgMessage" }, + type: "danger", + acceptButtonText: this.i18nService.t("continue"), + cancelButtonText: this.i18nService.t("close"), + }); + } + + private async navigateToPaymentMethod(orgId: string) { + await this.router.navigate(["organizations", `${orgId}`, "billing", "payment-method"], { + state: { launchPaymentModalAutomatically: true }, + }); + } +} diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts index 450c1234567..0c8e93531ee 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts @@ -74,6 +74,7 @@ export class AdjustPaymentDialogComponent { } }); await response; + await new Promise((resolve) => setTimeout(resolve, 10000)); this.toastService.showToast({ variant: "success", title: null, diff --git a/apps/web/src/app/billing/shared/billing-shared.module.ts b/apps/web/src/app/billing/shared/billing-shared.module.ts index 57491a73e6d..b9c235943ad 100644 --- a/apps/web/src/app/billing/shared/billing-shared.module.ts +++ b/apps/web/src/app/billing/shared/billing-shared.module.ts @@ -1,5 +1,7 @@ import { NgModule } from "@angular/core"; +import { BannerModule } from "@bitwarden/components"; + import { HeaderModule } from "../../layouts/header/header.module"; import { SharedModule } from "../../shared"; @@ -27,6 +29,7 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac PaymentComponent, TaxInfoComponent, HeaderModule, + BannerModule, PaymentV2Component, VerifyBankAccountComponent, ], diff --git a/apps/web/src/app/billing/shared/payment-method.component.html b/apps/web/src/app/billing/shared/payment-method.component.html index 495785af45f..1d4675847a1 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.html +++ b/apps/web/src/app/billing/shared/payment-method.component.html @@ -1,3 +1,23 @@ + + {{ freeTrialData?.message }} + + {{ "routeToPaymentMethodTrigger" | i18n }} + + +

    diff --git a/apps/web/src/app/billing/shared/payment-method.component.ts b/apps/web/src/app/billing/shared/payment-method.component.ts index 06acf2142a5..98e6efcd8bd 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.ts +++ b/apps/web/src/app/billing/shared/payment-method.component.ts @@ -1,10 +1,13 @@ -import { Component, OnInit, ViewChild } from "@angular/core"; +import { Location } from "@angular/common"; +import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, FormControl, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { lastValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { BillingPaymentResponse } from "@bitwarden/common/billing/models/response/billing-payment.response"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; @@ -13,8 +16,12 @@ import { VerifyBankRequest } from "@bitwarden/common/models/request/verify-bank. import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { DialogService, ToastService } from "@bitwarden/components"; +import { FreeTrial } from "../../core/types/free-trial"; +import { TrialFlowService } from "../services/trial-flow.service"; + import { AddCreditDialogResult, openAddCreditDialog } from "./add-credit-dialog.component"; import { AdjustPaymentDialogResult, @@ -26,7 +33,7 @@ import { TaxInfoComponent } from "./tax-info.component"; templateUrl: "payment-method.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class PaymentMethodComponent implements OnInit { +export class PaymentMethodComponent implements OnInit, OnDestroy { @ViewChild(TaxInfoComponent) taxInfo: TaxInfoComponent; loading = false; @@ -37,6 +44,7 @@ export class PaymentMethodComponent implements OnInit { paymentMethodType = PaymentMethodType; organizationId: string; isUnpaid = false; + organization: Organization; verifyBankForm = this.formBuilder.group({ amount1: new FormControl(null, [ @@ -52,6 +60,8 @@ export class PaymentMethodComponent implements OnInit { }); taxForm = this.formBuilder.group({}); + launchPaymentModalAutomatically = false; + protected freeTrialData: FreeTrial; constructor( protected apiService: ApiService, @@ -59,12 +69,30 @@ export class PaymentMethodComponent implements OnInit { protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, private router: Router, + private location: Location, private logService: LogService, private route: ActivatedRoute, private formBuilder: FormBuilder, private dialogService: DialogService, private toastService: ToastService, - ) {} + private trialFlowService: TrialFlowService, + private organizationService: OrganizationService, + protected syncService: SyncService, + ) { + const state = this.router.getCurrentNavigation()?.extras?.state; + // incase the above state is undefined or null we use redundantState + const redundantState: any = location.getState(); + if (state && Object.prototype.hasOwnProperty.call(state, "launchPaymentModalAutomatically")) { + this.launchPaymentModalAutomatically = state.launchPaymentModalAutomatically; + } else if ( + redundantState && + Object.prototype.hasOwnProperty.call(redundantState, "launchPaymentModalAutomatically") + ) { + this.launchPaymentModalAutomatically = redundantState.launchPaymentModalAutomatically; + } else { + this.launchPaymentModalAutomatically = false; + } + } async ngOnInit() { // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe @@ -88,27 +116,37 @@ export class PaymentMethodComponent implements OnInit { return; } this.loading = true; - if (this.forOrganization) { const billingPromise = this.organizationApiService.getBilling(this.organizationId); const organizationSubscriptionPromise = this.organizationApiService.getSubscription( this.organizationId, ); + const organizationPromise = this.organizationService.get(this.organizationId); - [this.billing, this.org] = await Promise.all([ + [this.billing, this.org, this.organization] = await Promise.all([ billingPromise, organizationSubscriptionPromise, + organizationPromise, ]); + this.determineOrgsWithUpcomingPaymentIssues(); } else { const billingPromise = this.apiService.getUserBillingPayment(); const subPromise = this.apiService.getUserSubscription(); [this.billing, this.sub] = await Promise.all([billingPromise, subPromise]); } - this.isUnpaid = this.subscription?.status === "unpaid" ?? false; - this.loading = false; + // If the flag `launchPaymentModalAutomatically` is set to true, + // we schedule a timeout (delay of 800ms) to automatically launch the payment modal. + // This delay ensures that any prior UI/rendering operations complete before triggering the modal. + if (this.launchPaymentModalAutomatically) { + window.setTimeout(async () => { + await this.changePayment(); + this.launchPaymentModalAutomatically = false; + this.location.replaceState(this.location.path(), "", {}); + }, 800); + } }; addCredit = async () => { @@ -132,6 +170,11 @@ export class PaymentMethodComponent implements OnInit { }); const result = await lastValueFrom(dialogRef.closed); if (result === AdjustPaymentDialogResult.Adjusted) { + this.location.replaceState(this.location.path(), "", {}); + if (this.launchPaymentModalAutomatically && !this.organization.enabled) { + await this.syncService.fullSync(true); + } + this.launchPaymentModalAutomatically = false; await this.load(); } }; @@ -162,6 +205,14 @@ export class PaymentMethodComponent implements OnInit { }); }; + determineOrgsWithUpcomingPaymentIssues() { + this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( + this.organization, + this.org, + this.billing?.paymentSource, + ); + } + get isCreditBalance() { return this.billing == null || this.billing.balance <= 0; } @@ -203,4 +254,8 @@ export class PaymentMethodComponent implements OnInit { get subscription() { return this.sub?.subscription ?? this.org?.subscription ?? null; } + + ngOnDestroy(): void { + this.launchPaymentModalAutomatically = false; + } } diff --git a/apps/web/src/app/core/event.service.ts b/apps/web/src/app/core/event.service.ts index 0bfe1d0dc89..be6a62443d3 100644 --- a/apps/web/src/app/core/event.service.ts +++ b/apps/web/src/app/core/event.service.ts @@ -330,6 +330,20 @@ export class EventService { this.getShortId(ev.organizationUserId), ); break; + case EventType.OrganizationUser_Deleted: + msg = this.i18nService.t("deletedUserId", this.formatOrgUserId(ev)); + humanReadableMsg = this.i18nService.t( + "deletedUserId", + this.getShortId(ev.organizationUserId), + ); + break; + case EventType.OrganizationUser_Left: + msg = this.i18nService.t("userLeftOrganization", this.formatOrgUserId(ev)); + humanReadableMsg = this.i18nService.t( + "userLeftOrganization", + this.getShortId(ev.organizationUserId), + ); + break; // Org case EventType.Organization_Updated: msg = humanReadableMsg = this.i18nService.t("editedOrgSettings"); diff --git a/apps/web/src/app/core/types/free-trial.ts b/apps/web/src/app/core/types/free-trial.ts new file mode 100644 index 00000000000..ee5fb921621 --- /dev/null +++ b/apps/web/src/app/core/types/free-trial.ts @@ -0,0 +1,7 @@ +export type FreeTrial = { + remainingDays: number; + message: string; + shownBanner: boolean; + organizationId: string; + organizationName: string; +}; diff --git a/apps/web/src/app/layouts/org-switcher/org-switcher.component.html b/apps/web/src/app/layouts/org-switcher/org-switcher.component.html index 1dd03d03230..f34d32f5983 100644 --- a/apps/web/src/app/layouts/org-switcher/org-switcher.component.html +++ b/apps/web/src/app/layouts/org-switcher/org-switcher.component.html @@ -22,6 +22,7 @@ [route]="['../', org.id]" (mainContentClicked)="toggle()" [routerLinkActiveOptions]="{ exact: true }" + (click)="handleUnpaidSubscription(org)" > - + {{ "application" | i18n }} {{ "atRiskPasswords" | i18n }} {{ "totalPasswords" | i18n }} @@ -78,7 +79,7 @@ - + - + {{ "criticalApplicationsWithCount" | i18n: criticalApps.length }} diff --git a/apps/web/src/app/tools/risk-insights/risk-insights.component.ts b/apps/web/src/app/tools/risk-insights/risk-insights.component.ts index 43d6da70e96..1c6a36b4454 100644 --- a/apps/web/src/app/tools/risk-insights/risk-insights.component.ts +++ b/apps/web/src/app/tools/risk-insights/risk-insights.component.ts @@ -1,9 +1,11 @@ import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components"; import { HeaderModule } from "../../layouts/header/header.module"; @@ -39,9 +41,10 @@ export enum RiskInsightsTabType { TabsModule, ], }) -export class RiskInsightsComponent { +export class RiskInsightsComponent implements OnInit { tabIndex: RiskInsightsTabType; dataLastUpdated = new Date(); + isCritialAppsFeatureEnabled = false; apps: any[] = []; criticalApps: any[] = []; @@ -65,9 +68,16 @@ export class RiskInsightsComponent { }); }; + async ngOnInit() { + this.isCritialAppsFeatureEnabled = await this.configService.getFeatureFlag( + FeatureFlag.CriticalApps, + ); + } + constructor( protected route: ActivatedRoute, private router: Router, + private configService: ConfigService, ) { route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => { this.tabIndex = !isNaN(tabIndex) ? tabIndex : RiskInsightsTabType.AllApps; diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index bf623e729a1..df575cc525f 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -466,7 +466,14 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { * Helper method to delete cipher. */ private async deleteCipher(): Promise { - const asAdmin = this.organization?.canEditAllCiphers; + const cipherIsUnassigned = + !this.cipher.collectionIds || this.cipher.collectionIds?.length === 0; + + // Delete the cipher as an admin when: + // - the organization allows for owners/admins to manage all collections/items + // - the cipher is unassigned + const asAdmin = this.organization?.canEditAllCiphers || cipherIsUnassigned; + if (this.cipher.isDeleted) { await this.cipherService.deleteWithServer(this.cipher.id, asAdmin); } else { diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.html b/apps/web/src/app/vault/individual-vault/add-edit.component.html index 855b5dac489..01ac60fc7e6 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit.component.html +++ b/apps/web/src/app/vault/individual-vault/add-edit.component.html @@ -851,6 +851,99 @@

    + + +
    +
    + +
    + +
    + + +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    +