From 872f36752f585740ab4ee996b924af41951f74ac Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Thu, 7 Nov 2024 15:03:54 +0100 Subject: [PATCH 01/47] [PM-13876] Generator PR review follow up (#11885) * Remove unused regex * Remove viewChild reference from markup --------- Co-authored-by: Daniel James Smith --- .../generator/components/src/catchall-settings.component.ts | 5 ----- .../components/src/credential-generator.component.html | 6 ------ .../components/src/password-generator.component.html | 2 -- .../components/src/username-generator.component.html | 4 ---- 4 files changed, 17 deletions(-) diff --git a/libs/tools/generator/components/src/catchall-settings.component.ts b/libs/tools/generator/components/src/catchall-settings.component.ts index 74fb37d233..3a42d68297 100644 --- a/libs/tools/generator/components/src/catchall-settings.component.ts +++ b/libs/tools/generator/components/src/catchall-settings.component.ts @@ -12,11 +12,6 @@ import { import { completeOnAccountSwitch } from "./util"; -/** Splits an email into a username, subaddress, and domain named group. - * Subaddress is optional. - */ -export const DOMAIN_PARSER = new RegExp("[^@]+@(?.+)"); - /** Options group for catchall emails */ @Component({ selector: "tools-catchall-settings", diff --git a/libs/tools/generator/components/src/credential-generator.component.html b/libs/tools/generator/components/src/credential-generator.component.html index 0182bd1c20..ce86abe80a 100644 --- a/libs/tools/generator/components/src/credential-generator.component.html +++ b/libs/tools/generator/components/src/credential-generator.component.html @@ -39,14 +39,12 @@ Date: Thu, 7 Nov 2024 10:10:15 -0500 Subject: [PATCH 02/47] [PM-11201][CL-507] Add the ability to sort by Name, Group, and Permission within the collection and item tables (#11453) * Added sorting to vault, name, permission and group Added default sorting * Fixed import * reverted test on template * Only add sorting functionality to admin console * changed code order * Fixed leftover test for sortingn * Fixed reference * sort permissions by ascending order * Fixed bug where a collection had multiple groups and sorting alphbatically didn't happen correctly all the time * Fixed bug whne creating a new cipher item * Introduced fnFactory to create a sort function with direction provided * Used new sort function to make collections always remain at the top and ciphers below * extracted logic to always sort collections at the top Added similar sorting to sortBygroup * removed org vault check * remove unused service * Sort only collections * Got rid of sortFn factory in favour of passing the direction as an optional parameter * Removed tenary * get cipher permissions * Use all collections to filter collection ids * Fixed ascending and descending issues * Added functionality to default sort in descending order * default sort permissions in descending order * Refactored setActive to not pass direction as a paramater --- .../vault-items/vault-items.component.html | 32 +++- .../vault-items/vault-items.component.ts | 177 +++++++++++++++++- .../src/table/sortable.component.ts | 31 ++- .../components/src/table/table-data-source.ts | 4 +- libs/components/src/table/table.mdx | 15 +- 5 files changed, 245 insertions(+), 14 deletions(-) diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.html b/apps/web/src/app/vault/components/vault-items/vault-items.component.html index d6bcd76903..653d05ef12 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.html @@ -16,13 +16,39 @@ "all" | i18n }} - {{ "name" | i18n }} + + + {{ "name" | i18n }} + + + + {{ "name" | i18n }} + {{ "owner" | i18n }} {{ "collections" | i18n }} - {{ "groups" | i18n }} - + + {{ "groups" | i18n }} + + {{ "permission" | i18n }} diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index 71a97f1ff4..9f19a0319a 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -1,13 +1,17 @@ import { SelectionModel } from "@angular/cdk/collections"; import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { CollectionView, Unassigned } from "@bitwarden/admin-console/common"; +import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { TableDataSource } from "@bitwarden/components"; +import { SortDirection, TableDataSource } from "@bitwarden/components"; import { GroupView } from "../../../admin-console/organizations/core"; +import { + CollectionPermission, + convertToPermission, +} from "./../../../admin-console/organizations/shared/components/access-selector/access-selector.models"; import { VaultItem } from "./vault-item"; import { VaultItemEvent } from "./vault-item-event"; @@ -17,6 +21,8 @@ export const RowHeightClass = `tw-h-[65px]`; const MaxSelectionCount = 500; +type ItemPermission = CollectionPermission | "NoAccess"; + @Component({ selector: "app-vault-items", templateUrl: "vault-items.component.html", @@ -333,6 +339,119 @@ export class VaultItemsComponent { return (canEditOrManageAllCiphers || this.allCiphersHaveEditAccess()) && collectionNotSelected; } + /** + * Sorts VaultItems, grouping collections before ciphers, and sorting each group alphabetically by name. + */ + protected sortByName = (a: VaultItem, b: VaultItem, direction: SortDirection) => { + // Collections before ciphers + const collectionCompare = this.prioritizeCollections(a, b, direction); + if (collectionCompare !== 0) { + return collectionCompare; + } + + return this.compareNames(a, b); + }; + + /** + * Sorts VaultItems based on group names + */ + protected sortByGroups = (a: VaultItem, b: VaultItem, direction: SortDirection) => { + if ( + !(a.collection instanceof CollectionAdminView) && + !(b.collection instanceof CollectionAdminView) + ) { + return 0; + } + + const getFirstGroupName = (collection: CollectionAdminView): string => { + if (collection.groups.length > 0) { + return collection.groups.map((group) => this.getGroupName(group.id) || "").sort()[0]; + } + return null; + }; + + // Collections before ciphers + const collectionCompare = this.prioritizeCollections(a, b, direction); + if (collectionCompare !== 0) { + return collectionCompare; + } + + const aGroupName = getFirstGroupName(a.collection as CollectionAdminView); + const bGroupName = getFirstGroupName(b.collection as CollectionAdminView); + + // Collections with groups come before collections without groups. + // If a collection has no groups, getFirstGroupName returns null. + if (aGroupName === null) { + return 1; + } + + if (bGroupName === null) { + return -1; + } + + return aGroupName.localeCompare(bGroupName); + }; + + /** + * Sorts VaultItems based on their permissions, with higher permissions taking precedence. + * If permissions are equal, it falls back to sorting by name. + */ + protected sortByPermissions = (a: VaultItem, b: VaultItem, direction: SortDirection) => { + const getPermissionPriority = (item: VaultItem): number => { + const permission = item.collection + ? this.getCollectionPermission(item.collection) + : this.getCipherPermission(item.cipher); + + const priorityMap = { + [CollectionPermission.Manage]: 5, + [CollectionPermission.Edit]: 4, + [CollectionPermission.EditExceptPass]: 3, + [CollectionPermission.View]: 2, + [CollectionPermission.ViewExceptPass]: 1, + NoAccess: 0, + }; + + return priorityMap[permission] ?? -1; + }; + + // Collections before ciphers + const collectionCompare = this.prioritizeCollections(a, b, direction); + if (collectionCompare !== 0) { + return collectionCompare; + } + + const priorityA = getPermissionPriority(a); + const priorityB = getPermissionPriority(b); + + // Higher priority first + if (priorityA !== priorityB) { + return priorityA - priorityB; + } + + return this.compareNames(a, b); + }; + + private compareNames(a: VaultItem, b: VaultItem): number { + const getName = (item: VaultItem) => item.collection?.name || item.cipher?.name; + return getName(a).localeCompare(getName(b)); + } + + /** + * Sorts VaultItems by prioritizing collections over ciphers. + * Collections are always placed before ciphers, regardless of the sorting direction. + */ + private prioritizeCollections(a: VaultItem, b: VaultItem, direction: SortDirection): number { + if (a.collection && !b.collection) { + return direction === "asc" ? -1 : 1; + } + + if (!a.collection && b.collection) { + return direction === "asc" ? 1 : -1; + } + + return 0; + } + private hasPersonalItems(): boolean { return this.selection.selected.some(({ cipher }) => cipher?.organizationId === null); } @@ -346,4 +465,58 @@ export class VaultItemsComponent { private getUniqueOrganizationIds(): Set { return new Set(this.selection.selected.flatMap((i) => i.cipher?.organizationId ?? [])); } + + private getGroupName(groupId: string): string | undefined { + return this.allGroups.find((g) => g.id === groupId)?.name; + } + + private getCollectionPermission(collection: CollectionView): ItemPermission { + const organization = this.allOrganizations.find((o) => o.id === collection.organizationId); + + if (collection.id == Unassigned && organization?.canEditUnassignedCiphers) { + return CollectionPermission.Edit; + } + + if (collection.assigned) { + return convertToPermission(collection); + } + + return "NoAccess"; + } + + private getCipherPermission(cipher: CipherView): ItemPermission { + if (!cipher.organizationId || cipher.collectionIds.length === 0) { + return CollectionPermission.Manage; + } + + const filteredCollections = this.allCollections?.filter((collection) => { + if (collection.assigned) { + return cipher.collectionIds.find((id) => { + if (collection.id === id) { + return collection; + } + }); + } + }); + + if (filteredCollections?.length === 1) { + return convertToPermission(filteredCollections[0]); + } + + if (filteredCollections?.length > 0) { + const permissions = filteredCollections.map((collection) => convertToPermission(collection)); + + const orderedPermissions = [ + CollectionPermission.Manage, + CollectionPermission.Edit, + CollectionPermission.EditExceptPass, + CollectionPermission.View, + CollectionPermission.ViewExceptPass, + ]; + + return orderedPermissions.find((perm) => permissions.includes(perm)); + } + + return "NoAccess"; + } } diff --git a/libs/components/src/table/sortable.component.ts b/libs/components/src/table/sortable.component.ts index b2e456953b..c6d60f155b 100644 --- a/libs/components/src/table/sortable.component.ts +++ b/libs/components/src/table/sortable.component.ts @@ -1,7 +1,7 @@ import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { Component, HostBinding, Input, OnInit } from "@angular/core"; -import type { SortFn } from "./table-data-source"; +import type { SortDirection, SortFn } from "./table-data-source"; import { TableComponent } from "./table.component"; @Component({ @@ -19,12 +19,16 @@ export class SortableComponent implements OnInit { */ @Input() bitSortable: string; - private _default: boolean; + private _default: SortDirection | boolean = false; /** * Mark the column as the default sort column */ - @Input() set default(value: boolean | "") { - this._default = coerceBooleanProperty(value); + @Input() set default(value: SortDirection | boolean | "") { + if (value === "desc" || value === "asc") { + this._default = value; + } else { + this._default = coerceBooleanProperty(value) ? "asc" : false; + } } /** @@ -32,6 +36,11 @@ export class SortableComponent implements OnInit { * * @example * fn = (a, b) => a.name.localeCompare(b.name) + * + * fn = (a, b, direction) => { + * const result = a.name.localeCompare(b.name) + * return direction === 'asc' ? result : -result; + * } */ @Input() fn: SortFn; @@ -52,8 +61,18 @@ export class SortableComponent implements OnInit { protected setActive() { if (this.table.dataSource) { - const direction = this.isActive && this.direction === "asc" ? "desc" : "asc"; - this.table.dataSource.sort = { column: this.bitSortable, direction: direction, fn: this.fn }; + const defaultDirection = this._default === "desc" ? "desc" : "asc"; + const direction = this.isActive + ? this.direction === "asc" + ? "desc" + : "asc" + : defaultDirection; + + this.table.dataSource.sort = { + column: this.bitSortable, + direction: direction, + fn: this.fn, + }; } } diff --git a/libs/components/src/table/table-data-source.ts b/libs/components/src/table/table-data-source.ts index 6501c9bffb..8a5d994dc1 100644 --- a/libs/components/src/table/table-data-source.ts +++ b/libs/components/src/table/table-data-source.ts @@ -3,7 +3,7 @@ import { DataSource } from "@angular/cdk/collections"; import { BehaviorSubject, combineLatest, map, Observable, Subscription } from "rxjs"; export type SortDirection = "asc" | "desc"; -export type SortFn = (a: any, b: any) => number; +export type SortFn = (a: any, b: any, direction?: SortDirection) => number; export type Sort = { column?: string; direction: SortDirection; @@ -166,7 +166,7 @@ export class TableDataSource extends DataSource { return data.sort((a, b) => { // If a custom sort function is provided, use it instead of the default. if (sort.fn) { - return sort.fn(a, b) * directionModifier; + return sort.fn(a, b, sort.direction) * directionModifier; } let valueA = this.sortingDataAccessor(a, column); diff --git a/libs/components/src/table/table.mdx b/libs/components/src/table/table.mdx index 3f28dd93b6..8d784190ed 100644 --- a/libs/components/src/table/table.mdx +++ b/libs/components/src/table/table.mdx @@ -105,7 +105,7 @@ within the `ng-template`which provides access to the rows using `let-rows$`. We provide a simple component for displaying sortable column headers. The `bitSortable` component wires up to the `TableDataSource` and will automatically sort the data when clicked and display an -indicator for which column is currently sorted. The dafault sorting can be specified by setting the +indicator for which column is currently sorted. The default sorting can be specified by setting the `default`. ```html @@ -113,10 +113,23 @@ indicator for which column is currently sorted. The dafault sorting can be speci Name ``` +For default sorting in descending order, set default="desc" + +```html +Name +``` + It's also possible to define a custom sorting function by setting the `fn` input. ```ts +// Basic sort function const sortFn = (a: T, b: T) => (a.id > b.id ? 1 : -1); + +// Direction aware sort function +const sortByName = (a: T, b: T, direction?: SortDirection) => { + const result = a.name.localeCompare(b.name); + return direction === "asc" ? result : -result; +}; ``` ### Filtering From 05a79d58bb1e7310261582827e730a26e7abd33f Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Thu, 7 Nov 2024 10:38:49 -0500 Subject: [PATCH 03/47] do not do browser extension permission check if the Show autofill suggestions setting is being turned off (#11889) --- .../src/autofill/popup/settings/autofill.component.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.ts b/apps/browser/src/autofill/popup/settings/autofill.component.ts index b395808f57..ac247609b1 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.ts +++ b/apps/browser/src/autofill/popup/settings/autofill.component.ts @@ -219,7 +219,11 @@ export class AutofillComponent implements OnInit { : AutofillOverlayVisibility.Off; await this.autofillSettingsService.setInlineMenuVisibility(newInlineMenuVisibilityValue); - await this.requestPrivacyPermission(); + + // No need to initiate browser permission request if a feature is being turned off + if (newInlineMenuVisibilityValue !== AutofillOverlayVisibility.Off) { + await this.requestPrivacyPermission(); + } } async updateAutofillOnPageLoad() { From b42741f313864496cfe30a55231cd63168c4da03 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:22:35 -0600 Subject: [PATCH 04/47] [PM-13839][PM-13840] Admin Console Collections (#11649) * allow admin console to see all collections when viewing a cipher - When "manage all" option is selected all collections should be editable * update cipher form service to use admin endpoints * when saving a cipher, choose to move to collections first before saving any other edits - This handles the case where a cipher is moving from unassigned to assigned and needs to have a collection to save any other edits * set admin flag when the original cipher has zero collections - handling the case where the user un-assigns themselves from a cipher * add check for the users ability to edit items within the collection * save cipher edit first to handle when the user unassigns themselves from the cipher * update filter order of collections * use cipher returned from the collections endpoint rather than re-fetching it * fix unit tests by adding canEditItems * re-enable collection control when orgId is present * fetch the updated cipher from the respective service for editing a cipher --- .../vault-item-dialog.component.ts | 19 ++- ...console-cipher-form-config.service.spec.ts | 64 ++++---- ...dmin-console-cipher-form-config.service.ts | 39 +---- libs/common/src/services/api.service.ts | 2 +- .../src/vault/abstractions/cipher.service.ts | 2 +- .../src/vault/services/cipher.service.ts | 6 +- .../item-details-section.component.spec.ts | 153 ++++++++++++++++-- .../item-details-section.component.ts | 22 ++- .../services/default-cipher-form.service.ts | 16 +- .../src/cipher-view/cipher-view.component.ts | 1 + 10 files changed, 240 insertions(+), 84 deletions(-) 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 ae2cf88fd1..bf623e729a 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 @@ -6,6 +6,7 @@ import { firstValueFrom, Observable, Subject } from "rxjs"; import { map } from "rxjs/operators"; import { CollectionView } from "@bitwarden/admin-console/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; @@ -17,6 +18,8 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { @@ -231,6 +234,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { private billingAccountProfileStateService: BillingAccountProfileStateService, private premiumUpgradeService: PremiumUpgradePromptService, private cipherAuthorizationService: CipherAuthorizationService, + private apiService: ApiService, ) { this.updateTitle(); } @@ -278,7 +282,20 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { if (this._originalFormMode === "add" || this._originalFormMode === "clone") { this.formConfig.mode = "edit"; } - this.formConfig.originalCipher = await this.cipherService.get(cipherView.id); + + let cipher: Cipher; + + // When the form config is used within the Admin Console, retrieve the cipher from the admin endpoint + if (this.formConfig.isAdminConsole) { + const cipherResponse = await this.apiService.getCipherAdmin(cipherView.id); + const cipherData = new CipherData(cipherResponse); + cipher = new Cipher(cipherData); + } else { + cipher = await this.cipherService.get(cipherView.id); + } + + // Store the updated cipher so any following edits use the most up to date cipher + this.formConfig.originalCipher = cipher; this._cipherModified = true; await this.changeMode("view"); } diff --git a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts index 02d280f5ff..05c40fe2e7 100644 --- a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts +++ b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts @@ -1,14 +1,13 @@ import { TestBed } from "@angular/core/testing"; import { BehaviorSubject } from "rxjs"; -import { CollectionAdminService } from "@bitwarden/admin-console/common"; +import { CollectionAdminService, CollectionAdminView } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CipherId } from "@bitwarden/common/types/guid"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; @@ -35,27 +34,41 @@ describe("AdminConsoleCipherFormConfigService", () => { status: OrganizationUserStatusType.Confirmed, }; const policyAppliesToActiveUser$ = new BehaviorSubject(true); + const collection = { + id: "12345-5555", + organizationId: "234534-34334", + name: "Test Collection 1", + assigned: false, + readOnly: true, + } as CollectionAdminView; + const collection2 = { + id: "12345-6666", + organizationId: "22222-2222", + name: "Test Collection 2", + assigned: true, + readOnly: false, + } as CollectionAdminView; + const organization$ = new BehaviorSubject(testOrg as Organization); const organizations$ = new BehaviorSubject([testOrg, testOrg2] as Organization[]); const getCipherAdmin = jest.fn().mockResolvedValue(null); - const getCipher = jest.fn().mockResolvedValue(null); beforeEach(async () => { getCipherAdmin.mockClear(); - getCipher.mockClear(); - getCipher.mockResolvedValue({ id: cipherId, name: "Test Cipher - (non-admin)" }); getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" }); await TestBed.configureTestingModule({ providers: [ AdminConsoleCipherFormConfigService, + { provide: OrganizationService, useValue: { get$: () => organization$, organizations$ } }, + { + provide: CollectionAdminService, + useValue: { getAll: () => Promise.resolve([collection, collection2]) }, + }, { provide: PolicyService, useValue: { policyAppliesToActiveUser$: () => policyAppliesToActiveUser$ }, }, - { provide: OrganizationService, useValue: { get$: () => organization$, organizations$ } }, - { provide: CipherService, useValue: { get: getCipher } }, - { provide: CollectionAdminService, useValue: { getAll: () => Promise.resolve([]) } }, { provide: RoutedVaultFilterService, useValue: { filter$: new BehaviorSubject({ organizationId: testOrg.id }) }, @@ -86,6 +99,12 @@ describe("AdminConsoleCipherFormConfigService", () => { expect(mode).toBe("edit"); }); + it("returns all collections", async () => { + const { collections } = await adminConsoleConfigService.buildConfig("edit", cipherId); + + expect(collections).toEqual([collection, collection2]); + }); + it("sets admin flag based on `canEditAllCiphers`", async () => { // Disable edit all ciphers on org testOrg.canEditAllCiphers = false; @@ -153,33 +172,14 @@ describe("AdminConsoleCipherFormConfigService", () => { expect(result.organizations).toEqual([testOrg, testOrg2]); }); - describe("getCipher", () => { - it("retrieves the cipher from the cipher service", async () => { - testOrg.canEditAllCiphers = false; + it("retrieves the cipher from the admin service", async () => { + getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" }); - adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); + adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); - const result = await adminConsoleConfigService.buildConfig("clone", cipherId); + await adminConsoleConfigService.buildConfig("add", cipherId); - expect(getCipher).toHaveBeenCalledWith(cipherId); - expect(result.originalCipher.name).toBe("Test Cipher - (non-admin)"); - - // Admin service not needed when cipher service can return the cipher - expect(getCipherAdmin).not.toHaveBeenCalled(); - }); - - it("retrieves the cipher from the admin service", async () => { - getCipher.mockResolvedValueOnce(null); - getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" }); - - adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); - - await adminConsoleConfigService.buildConfig("add", cipherId); - - expect(getCipherAdmin).toHaveBeenCalledWith(cipherId); - - expect(getCipher).toHaveBeenCalledWith(cipherId); - }); + expect(getCipherAdmin).toHaveBeenCalledWith(cipherId); }); }); }); diff --git a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts index 328ab4475d..457b4e83d0 100644 --- a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts +++ b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts @@ -6,9 +6,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType, OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CipherId } from "@bitwarden/common/types/guid"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; @@ -25,7 +23,6 @@ import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/se export class AdminConsoleCipherFormConfigService implements CipherFormConfigService { private policyService: PolicyService = inject(PolicyService); private organizationService: OrganizationService = inject(OrganizationService); - private cipherService: CipherService = inject(CipherService); private routedVaultFilterService: RoutedVaultFilterService = inject(RoutedVaultFilterService); private collectionAdminService: CollectionAdminService = inject(CollectionAdminService); private apiService: ApiService = inject(ApiService); @@ -51,20 +48,8 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ map(([orgs, orgId]) => orgs.find((o) => o.id === orgId)), ); - private editableCollections$ = this.organization$.pipe( - switchMap(async (org) => { - if (!org) { - return []; - } - - const collections = await this.collectionAdminService.getAll(org.id); - // Users that can edit all ciphers can implicitly add to / edit within any collection - if (org.canEditAllCiphers) { - return collections; - } - // The user is only allowed to add/edit items to assigned collections that are not readonly - return collections.filter((c) => c.assigned && !c.readOnly); - }), + private allCollections$ = this.organization$.pipe( + switchMap(async (org) => await this.collectionAdminService.getAll(org.id)), ); async buildConfig( @@ -72,21 +57,17 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ cipherId?: CipherId, cipherType?: CipherType, ): Promise { + const cipher = await this.getCipher(cipherId); const [organization, allowPersonalOwnership, allOrganizations, allCollections] = await firstValueFrom( combineLatest([ this.organization$, this.allowPersonalOwnership$, this.allOrganizations$, - this.editableCollections$, + this.allCollections$, ]), ); - const cipher = await this.getCipher(organization, cipherId); - - const collections = allCollections.filter( - (c) => c.organizationId === organization.id && c.assigned && !c.readOnly, - ); // When cloning from within the Admin Console, all organizations should be available. // Otherwise only the one in context should be const organizations = mode === "clone" ? allOrganizations : [organization]; @@ -100,7 +81,7 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ admin: organization.canEditAllCiphers ?? false, allowPersonalOwnership: allowPersonalOwnershipOnlyForClone, originalCipher: cipher, - collections, + collections: allCollections, organizations, folders: [], // folders not applicable in the admin console hideIndividualVaultFields: true, @@ -108,19 +89,11 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ }; } - private async getCipher(organization: Organization, id?: CipherId): Promise { + private async getCipher(id?: CipherId): Promise { if (id == null) { return Promise.resolve(null); } - // Check to see if the user has direct access to the cipher - const cipherFromCipherService = await this.cipherService.get(id); - - // If the organization doesn't allow admin/owners to edit all ciphers return the cipher - if (!organization.canEditAllCiphers && cipherFromCipherService != null) { - return cipherFromCipherService; - } - // Retrieve the cipher through the means of an admin const cipherResponse = await this.apiService.getCipherAdmin(id); cipherResponse.edit = true; diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 2d4a052263..f9e05e7635 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -584,7 +584,7 @@ export class ApiService implements ApiServiceAbstraction { } putCipherCollectionsAdmin(id: string, request: CipherCollectionsRequest): Promise { - return this.send("PUT", "/ciphers/" + id + "/collections-admin", request, true, false); + return this.send("PUT", "/ciphers/" + id + "/collections-admin", request, true, true); } postPurgeCiphers( diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 444c922fe3..5221f4cf0a 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -119,7 +119,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider Promise; + saveCollectionsWithServerAdmin: (cipher: Cipher) => Promise; /** * Bulk update collections for many ciphers with the server * @param orgId diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 154042601e..6b618e2550 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -880,9 +880,11 @@ export class CipherService implements CipherServiceAbstraction { return new Cipher(updated[cipher.id as CipherId], cipher.localData); } - async saveCollectionsWithServerAdmin(cipher: Cipher): Promise { + async saveCollectionsWithServerAdmin(cipher: Cipher): Promise { const request = new CipherCollectionsRequest(cipher.collectionIds); - await this.apiService.putCipherCollectionsAdmin(cipher.id, request); + const response = await this.apiService.putCipherCollectionsAdmin(cipher.id, request); + const data = new CipherData(response); + return new Cipher(data); } /** diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts index b62557a432..93229bda6c 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts @@ -87,7 +87,12 @@ describe("ItemDetailsSectionComponent", () => { component.config.allowPersonalOwnership = true; component.config.organizations = [{ id: "org1" } as Organization]; component.config.collections = [ - { id: "col1", name: "Collection 1", organizationId: "org1" } as CollectionView, + { + id: "col1", + name: "Collection 1", + organizationId: "org1", + canEditItems: (_org) => true, + } as CollectionView, ]; component.originalCipherView = { name: "cipher1", @@ -116,8 +121,18 @@ describe("ItemDetailsSectionComponent", () => { component.config.allowPersonalOwnership = true; component.config.organizations = [{ id: "org1" } as Organization]; component.config.collections = [ - { id: "col1", name: "Collection 1", organizationId: "org1" } as CollectionView, - { id: "col2", name: "Collection 2", organizationId: "org1" } as CollectionView, + { + id: "col1", + name: "Collection 1", + organizationId: "org1", + canEditItems: (_org) => false, + } as CollectionView, + { + id: "col2", + name: "Collection 2", + organizationId: "org1", + canEditItems: (_org) => true, + } as CollectionView, ]; component.originalCipherView = { name: "cipher1", @@ -367,9 +382,24 @@ describe("ItemDetailsSectionComponent", () => { } as CipherView; component.config.organizations = [{ id: "org1" } as Organization]; component.config.collections = [ - { id: "col1", name: "Collection 1", organizationId: "org1" } as CollectionView, - { id: "col2", name: "Collection 2", organizationId: "org1" } as CollectionView, - { id: "col3", name: "Collection 3", organizationId: "org1" } as CollectionView, + { + id: "col1", + name: "Collection 1", + organizationId: "org1", + canEditItems: (_org) => true, + } as CollectionView, + { + id: "col2", + name: "Collection 2", + organizationId: "org1", + canEditItems: (_org) => true, + } as CollectionView, + { + id: "col3", + name: "Collection 3", + organizationId: "org1", + canEditItems: (_org) => true, + } as CollectionView, ]; fixture.detectChanges(); @@ -387,7 +417,12 @@ describe("ItemDetailsSectionComponent", () => { component.config.allowPersonalOwnership = true; component.config.organizations = [{ id: "org1" } as Organization]; component.config.collections = [ - { id: "col1", name: "Collection 1", organizationId: "org1" } as CollectionView, + { + id: "col1", + name: "Collection 1", + organizationId: "org1", + canEditItems: (_org) => true, + } as CollectionView, ]; fixture.detectChanges(); @@ -414,13 +449,24 @@ describe("ItemDetailsSectionComponent", () => { } as CipherView; component.config.organizations = [{ id: "org1" } as Organization]; component.config.collections = [ - { id: "col1", name: "Collection 1", organizationId: "org1" } as CollectionView, - { id: "col2", name: "Collection 2", organizationId: "org1" } as CollectionView, + { + id: "col1", + name: "Collection 1", + organizationId: "org1", + canEditItems: (_org) => true, + } as CollectionView, + { + id: "col2", + name: "Collection 2", + organizationId: "org1", + canEditItems: (_org) => true, + } as CollectionView, { id: "col3", name: "Collection 3", organizationId: "org1", readOnly: true, + canEditItems: (_org) => true, } as CollectionView, ]; @@ -433,5 +479,94 @@ describe("ItemDetailsSectionComponent", () => { expect(collectionHint).not.toBeNull(); }); + + it("should allow all collections to be altered when `config.admin` is true", async () => { + component.config.admin = true; + component.config.allowPersonalOwnership = true; + component.config.organizations = [{ id: "org1" } as Organization]; + component.config.collections = [ + { + id: "col1", + name: "Collection 1", + organizationId: "org1", + readOnly: true, + canEditItems: (_org) => false, + } as CollectionView, + { + id: "col2", + name: "Collection 2", + organizationId: "org1", + readOnly: true, + canEditItems: (_org) => false, + } as CollectionView, + { + id: "col3", + name: "Collection 3", + organizationId: "org1", + readOnly: false, + canEditItems: (_org) => false, + } as CollectionView, + ]; + + fixture.detectChanges(); + await fixture.whenStable(); + + component.itemDetailsForm.controls.organizationId.setValue("org1"); + + expect(component["collectionOptions"].map((c) => c.id)).toEqual(["col1", "col2", "col3"]); + }); + }); + + describe("readonlyCollections", () => { + beforeEach(() => { + component.config.mode = "edit"; + component.config.admin = true; + component.config.collections = [ + { + id: "col1", + name: "Collection 1", + organizationId: "org1", + readOnly: true, + canEditItems: (_org) => false, + } as CollectionView, + { + id: "col2", + name: "Collection 2", + organizationId: "org1", + canEditItems: (_org) => false, + } as CollectionView, + { + id: "col3", + name: "Collection 3", + organizationId: "org1", + readOnly: true, + canEditItems: (_org) => false, + } as CollectionView, + ]; + component.originalCipherView = { + name: "cipher1", + organizationId: "org1", + folderId: "folder1", + collectionIds: ["col1", "col2", "col3"], + favorite: true, + } as CipherView; + component.config.organizations = [{ id: "org1" } as Organization]; + }); + + it("should not show collections as readonly when `config.admin` is true", async () => { + await component.ngOnInit(); + fixture.detectChanges(); + + // Filters out all collections + expect(component["readOnlyCollections"]).toEqual([]); + + // Non-admin, keep readonly collections + component.config.admin = false; + + await component.ngOnInit(); + fixture.detectChanges(); + + expect(component["readOnlyCollections"]).toEqual(["Collection 1", "Collection 3"]); + }); }); }); diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts index 86a8818bbe..ea82aa0cae 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts @@ -240,7 +240,11 @@ export class ItemDetailsSectionComponent implements OnInit { } else if (this.config.mode === "edit") { this.readOnlyCollections = this.collections .filter( - (c) => c.readOnly && this.originalCipherView.collectionIds.includes(c.id as CollectionId), + // When the configuration is set up for admins, they can alter read only collections + (c) => + c.readOnly && + !this.config.admin && + this.originalCipherView.collectionIds.includes(c.id as CollectionId), ) .map((c) => c.name); } @@ -262,12 +266,24 @@ export class ItemDetailsSectionComponent implements OnInit { collectionsControl.disable(); this.showCollectionsControl = false; return; + } else { + collectionsControl.enable(); + this.showCollectionsControl = true; } + const organization = this.organizations.find((o) => o.id === orgId); + this.collectionOptions = this.collections .filter((c) => { - // If partial edit mode, show all org collections because the control is disabled. - return c.organizationId === orgId && (this.partialEdit || !c.readOnly); + // Filter criteria: + // - The collection belongs to the organization + // - When in partial edit mode, show all org collections because the control is disabled. + // - The user can edit items within the collection + // - When viewing as an admin, all collections should be shown, even readonly. When non-admin, filter out readonly collections + return ( + c.organizationId === orgId && + (this.partialEdit || c.canEditItems(organization) || this.config.admin) + ); }) .map((c) => ({ id: c.id, diff --git a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts index 8e73d9edd4..1b7e86f82a 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts @@ -1,6 +1,7 @@ import { inject, Injectable } from "@angular/core"; import { firstValueFrom, map } from "rxjs"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; @@ -17,6 +18,7 @@ function isSetEqual(a: Set, b: Set) { export class DefaultCipherFormService implements CipherFormService { private cipherService: CipherService = inject(CipherService); private accountService: AccountService = inject(AccountService); + private apiService: ApiService = inject(ApiService); async decryptCipher(cipher: Cipher): Promise { const activeUserId = await firstValueFrom( @@ -66,11 +68,21 @@ export class DefaultCipherFormService implements CipherFormService { // Updating a cipher with collection changes is not supported with a single request currently // First update the cipher with the original collectionIds encryptedCipher.collectionIds = config.originalCipher.collectionIds; - await this.cipherService.updateWithServer(encryptedCipher, config.admin); + await this.cipherService.updateWithServer( + encryptedCipher, + config.admin || originalCollectionIds.size === 0, + config.mode !== "clone", + ); // Then save the new collection changes separately encryptedCipher.collectionIds = cipher.collectionIds; - savedCipher = await this.cipherService.saveCollectionsWithServer(encryptedCipher); + + if (config.admin || originalCollectionIds.size === 0) { + // When using an admin config or the cipher was unassigned, update collections as an admin + savedCipher = await this.cipherService.saveCollectionsWithServerAdmin(encryptedCipher); + } else { + savedCipher = await this.cipherService.saveCollectionsWithServer(encryptedCipher); + } } // Its possible the cipher was made no longer available due to collection assignment changes diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index 5d61caf52f..0871fd8e78 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -98,6 +98,7 @@ export class CipherViewComponent implements OnChanges, OnDestroy { async loadCipherData() { // Load collections if not provided and the cipher has collectionIds if ( + this.cipher.collectionIds && this.cipher.collectionIds.length > 0 && (!this.collections || this.collections.length === 0) ) { From db40f20160f9f2b0136fc82176a051368dd990fe Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Thu, 7 Nov 2024 13:01:54 -0500 Subject: [PATCH 05/47] Check run permissions for build artifact generation secrets usage (#11897) --- .github/workflows/build-browser.yml | 28 ++++++++++++---- .github/workflows/build-cli.yml | 25 ++++++++++---- .github/workflows/build-desktop.yml | 51 ++++++++++++++++++++++------- .github/workflows/build-web.yml | 32 ++++++++++++++---- 4 files changed, 105 insertions(+), 31 deletions(-) diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 34c69912f5..647f198081 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -1,7 +1,8 @@ name: Build Browser on: - pull_request: + pull_request_target: + types: [opened, synchronize] branches-ignore: - 'l10n_master' - 'cf-pages' @@ -33,6 +34,10 @@ defaults: shell: bash jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + setup: name: Setup runs-on: ubuntu-22.04 @@ -41,8 +46,10 @@ jobs: adj_build_number: ${{ steps.gen_vars.outputs.adj_build_number }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Get Package Version id: gen_vars @@ -71,8 +78,10 @@ jobs: run: working-directory: apps/browser steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Testing locales - extName length run: | @@ -109,8 +118,10 @@ jobs: _BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -225,12 +236,15 @@ jobs: needs: - setup - locales-test + - check-run env: _BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -342,8 +356,10 @@ jobs: - build - build-safari steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 7994e508b3..1b0679dfbe 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -1,7 +1,8 @@ name: Build CLI on: - pull_request: + pull_request_target: + types: [opened, synchronize] branches-ignore: - 'l10n_master' - 'cf-pages' @@ -34,6 +35,10 @@ defaults: working-directory: apps/cli jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + setup: name: Setup runs-on: ubuntu-22.04 @@ -41,8 +46,10 @@ jobs: package_version: ${{ steps.retrieve-package-version.outputs.package_version }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Get Package Version id: retrieve-package-version @@ -58,7 +65,6 @@ jobs: NODE_VERSION=${NODE_NVMRC/v/''} echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT - cli: name: "${{ matrix.os.base }} - ${{ matrix.license_type.readable }}" strategy: @@ -82,8 +88,10 @@ jobs: _WIN_PKG_FETCH_VERSION: 20.11.1 _WIN_PKG_VERSION: 3.5 steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Setup Unix Vars run: | @@ -160,8 +168,10 @@ jobs: _WIN_PKG_FETCH_VERSION: 20.11.1 _WIN_PKG_VERSION: 3.5 steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Setup Windows builder run: | @@ -310,8 +320,10 @@ jobs: env: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Print environment run: | @@ -386,6 +398,7 @@ jobs: - cli - cli-windows - snap + - check-run steps: - name: Check if any job failed working-directory: ${{ github.workspace }} diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 4667a93711..ca64dbc0a4 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -1,7 +1,8 @@ name: Build Desktop on: - pull_request: + pull_request_target: + types: [opened, synchronize] branches-ignore: - 'l10n_master' - 'cf-pages' @@ -32,12 +33,18 @@ defaults: shell: bash jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + electron-verify: name: Verify Electron Version runs-on: ubuntu-22.04 steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Verify run: | @@ -65,8 +72,10 @@ jobs: run: working-directory: apps/desktop steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Get Package Version id: retrieve-version @@ -138,8 +147,10 @@ jobs: run: working-directory: apps/desktop steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -238,7 +249,9 @@ jobs: windows: name: Windows Build runs-on: windows-2022 - needs: setup + needs: + - setup + - check-run defaults: run: shell: pwsh @@ -248,8 +261,10 @@ jobs: _NODE_VERSION: ${{ needs.setup.outputs.node_version }} NODE_OPTIONS: --max_old_space_size=4096 steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -447,7 +462,9 @@ jobs: macos-build: name: MacOS Build runs-on: macos-13 - needs: setup + needs: + - setup + - check-run env: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} @@ -456,8 +473,10 @@ jobs: run: working-directory: apps/desktop steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -622,8 +641,10 @@ jobs: run: working-directory: apps/desktop steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -841,8 +862,10 @@ jobs: run: working-directory: apps/desktop steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -1088,8 +1111,10 @@ jobs: run: working-directory: apps/desktop steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -1279,8 +1304,10 @@ jobs: - macos-package-mas runs-on: ubuntu-22.04 steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 31f800d5b3..be2789dd1d 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -1,7 +1,8 @@ name: Build Web on: - pull_request: + pull_request_target: + types: [opened, synchronize] branches-ignore: - 'l10n_master' - 'cf-pages' @@ -36,6 +37,10 @@ env: _AZ_REGISTRY: bitwardenprod.azurecr.io jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + setup: name: Setup runs-on: ubuntu-22.04 @@ -43,8 +48,10 @@ jobs: version: ${{ steps.version.outputs.value }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Get GitHub sha as version id: version @@ -89,8 +96,10 @@ jobs: git_metadata: true steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -142,6 +151,7 @@ jobs: needs: - setup - build-artifacts + - check-run strategy: fail-fast: false matrix: @@ -155,8 +165,10 @@ jobs: env: _VERSION: ${{ needs.setup.outputs.version }} steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Check Branch to Publish env: @@ -250,11 +262,15 @@ jobs: crowdin-push: name: Crowdin Push if: github.ref == 'refs/heads/main' - needs: build-artifacts + needs: + - build-artifacts + - check-run runs-on: ubuntu-22.04 steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -284,7 +300,9 @@ jobs: name: Trigger web vault deploy if: github.ref == 'refs/heads/main' runs-on: ubuntu-22.04 - needs: build-artifacts + needs: + - build-artifacts + - check-run steps: - name: Login to Azure - CI Subscription uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 From ef0fd6067695d8f022c06421c903d148f796f4fe Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Thu, 7 Nov 2024 14:09:56 -0500 Subject: [PATCH 06/47] [PM-11409] prevent managed user from leaving organization (#11895) * add check to prevent managed user from leaving managing org * remove unused vaiable * add null check --- .../organization-options.component.ts | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts index 3b7db72a09..57eb9b1bdd 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts @@ -1,5 +1,5 @@ import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; -import { combineLatest, map, Observable, Subject, takeUntil } from "rxjs"; +import { combineLatest, map, Observable, of, Subject, switchMap, takeUntil } from "rxjs"; import { OrganizationUserApiService, @@ -8,11 +8,14 @@ import { import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -53,6 +56,8 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { private resetPasswordService: OrganizationUserResetPasswordService, private userVerificationService: UserVerificationService, private toastService: ToastService, + private configService: ConfigService, + private organizationService: OrganizationService, ) {} async ngOnInit() { @@ -60,23 +65,39 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { map((policies) => policies.filter((policy) => policy.type === PolicyType.ResetPassword)), ); + const managingOrg$ = this.configService + .getFeatureFlag$(FeatureFlag.AccountDeprovisioning) + .pipe( + switchMap((isAccountDeprovisioningEnabled) => + isAccountDeprovisioningEnabled + ? this.organizationService.organizations$.pipe( + map((organizations) => + organizations.find((o) => o.userIsManagedByOrganization === true), + ), + ) + : of(null), + ), + ); + combineLatest([ this.organization$, resetPasswordPolicies$, this.userDecryptionOptionsService.userDecryptionOptions$, + managingOrg$, ]) .pipe(takeUntil(this.destroy$)) - .subscribe(([organization, resetPasswordPolicies, decryptionOptions]) => { + .subscribe(([organization, resetPasswordPolicies, decryptionOptions, managingOrg]) => { this.organization = organization; this.resetPasswordPolicy = resetPasswordPolicies.find( (p) => p.organizationId === organization.id, ); - // A user can leave an organization if they are NOT using TDE and Key Connector, or they have a master password. + // A user can leave an organization if they are NOT a managed user and they are NOT using TDE and Key Connector, or they have a master password. this.showLeaveOrgOption = - (decryptionOptions.trustedDeviceOption == undefined && + managingOrg?.id !== organization.id && + ((decryptionOptions.trustedDeviceOption == undefined && decryptionOptions.keyConnectorOption == undefined) || - decryptionOptions.hasMasterPassword; + decryptionOptions.hasMasterPassword); // Hide the 3 dot menu if the user has no available actions this.hideMenu = From 668ede2dfb2ea7b4a34c6ce4e433f29d6f223545 Mon Sep 17 00:00:00 2001 From: Vince Grassia <593223+vgrassia@users.noreply.github.com> Date: Thu, 7 Nov 2024 14:38:05 -0500 Subject: [PATCH 07/47] Add event_name check to Deploy Web trigger job (#11901) --- .github/workflows/build-web.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index be2789dd1d..46dff21433 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -298,7 +298,7 @@ jobs: trigger-web-vault-deploy: name: Trigger web vault deploy - if: github.ref == 'refs/heads/main' + if: github.event_name != pull_request_target && github.ref == 'refs/heads/main' runs-on: ubuntu-22.04 needs: - build-artifacts From 771bfdaccd699efdc7c4b6a433ce296d93524a5a Mon Sep 17 00:00:00 2001 From: Vince Grassia <593223+vgrassia@users.noreply.github.com> Date: Thu, 7 Nov 2024 14:42:10 -0500 Subject: [PATCH 08/47] Fix quotes (#11902) --- .github/workflows/build-web.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 46dff21433..bb6909f986 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -298,7 +298,7 @@ jobs: trigger-web-vault-deploy: name: Trigger web vault deploy - if: github.event_name != pull_request_target && github.ref == 'refs/heads/main' + if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' runs-on: ubuntu-22.04 needs: - build-artifacts From e95af8269f6160bf66e48e213ae26a00f363ba18 Mon Sep 17 00:00:00 2001 From: Vince Grassia <593223+vgrassia@users.noreply.github.com> Date: Thu, 7 Nov 2024 15:15:44 -0500 Subject: [PATCH 09/47] Add check for trigger event (#11904) --- .github/workflows/build-browser.yml | 5 ++++- .github/workflows/build-cli.yml | 5 ++++- .github/workflows/build-desktop.yml | 5 ++++- .github/workflows/build-web.yml | 5 ++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 647f198081..ecd1e40494 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -397,7 +397,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') + && 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 1b0679dfbe..98ba5b9fd8 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -402,7 +402,10 @@ jobs: steps: - name: Check if any job failed working-directory: ${{ github.workspace }} - if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure') + if: | + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') + && 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 ca64dbc0a4..83389c5bbe 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -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') + && 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 bb6909f986..4ce5bad790 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -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') + && contains(needs.*.result, 'failure') run: exit 1 - name: Login to Azure - Prod Subscription From b2811e07ceabebb2b6329e53feacd6bc2cf194f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Thu, 7 Nov 2024 15:23:01 -0500 Subject: [PATCH 10/47] [PM-14198] zero minimums when the character category is disabled (#11906) --- .../core/src/policies/constraints.ts | 2 ++ ...ynamic-password-policy-constraints.spec.ts | 26 ++++--------------- .../dynamic-password-policy-constraints.ts | 6 ++--- 3 files changed, 10 insertions(+), 24 deletions(-) diff --git a/libs/tools/generator/core/src/policies/constraints.ts b/libs/tools/generator/core/src/policies/constraints.ts index 6071b57048..d320329938 100644 --- a/libs/tools/generator/core/src/policies/constraints.ts +++ b/libs/tools/generator/core/src/policies/constraints.ts @@ -2,6 +2,7 @@ import { Constraint } from "@bitwarden/common/tools/types"; import { sum } from "../util"; +const Zero: Constraint = { min: 0, max: 0 }; const AtLeastOne: Constraint = { min: 1 }; const RequiresTrue: Constraint = { requiredValue: true }; @@ -159,6 +160,7 @@ export { enforceConstant, readonlyTrueWhen, fitLength, + Zero, AtLeastOne, RequiresTrue, }; diff --git a/libs/tools/generator/core/src/policies/dynamic-password-policy-constraints.spec.ts b/libs/tools/generator/core/src/policies/dynamic-password-policy-constraints.spec.ts index 96f590f8ed..d05d75ffb7 100644 --- a/libs/tools/generator/core/src/policies/dynamic-password-policy-constraints.spec.ts +++ b/libs/tools/generator/core/src/policies/dynamic-password-policy-constraints.spec.ts @@ -1,6 +1,6 @@ import { DefaultPasswordBoundaries, DefaultPasswordGenerationOptions, Policies } from "../data"; -import { AtLeastOne } from "./constraints"; +import { AtLeastOne, Zero } from "./constraints"; import { DynamicPasswordPolicyConstraints } from "./dynamic-password-policy-constraints"; describe("DynamicPasswordPolicyConstraints", () => { @@ -207,7 +207,7 @@ describe("DynamicPasswordPolicyConstraints", () => { expect(calibrated.constraints.minNumber).toEqual(dynamic.constraints.minNumber); }); - it("disables the minNumber constraint when the state's number flag is false", () => { + it("outputs the zero constraint when the state's number flag is false", () => { const dynamic = new DynamicPasswordPolicyConstraints(Policies.Password.disabledValue); const state = { ...DefaultPasswordGenerationOptions, @@ -216,7 +216,7 @@ describe("DynamicPasswordPolicyConstraints", () => { const calibrated = dynamic.calibrate(state); - expect(calibrated.constraints.minNumber).toBeUndefined(); + expect(calibrated.constraints.minNumber).toEqual(Zero); }); it("outputs the minSpecial constraint when the state's special flag is true", () => { @@ -231,7 +231,7 @@ describe("DynamicPasswordPolicyConstraints", () => { expect(calibrated.constraints.minSpecial).toEqual(dynamic.constraints.minSpecial); }); - it("disables the minSpecial constraint when the state's special flag is false", () => { + it("outputs the zero constraint when the state's special flag is false", () => { const dynamic = new DynamicPasswordPolicyConstraints(Policies.Password.disabledValue); const state = { ...DefaultPasswordGenerationOptions, @@ -240,23 +240,7 @@ describe("DynamicPasswordPolicyConstraints", () => { const calibrated = dynamic.calibrate(state); - expect(calibrated.constraints.minSpecial).toBeUndefined(); - }); - - it("copies the minimum length constraint", () => { - const dynamic = new DynamicPasswordPolicyConstraints(Policies.Password.disabledValue); - - const calibrated = dynamic.calibrate(DefaultPasswordGenerationOptions); - - expect(calibrated.constraints.minSpecial).toBeUndefined(); - }); - - it("overrides the minimum length constraint when it is less than the sum of the state's minimums", () => { - const dynamic = new DynamicPasswordPolicyConstraints(Policies.Password.disabledValue); - - const calibrated = dynamic.calibrate(DefaultPasswordGenerationOptions); - - expect(calibrated.constraints.minSpecial).toBeUndefined(); + expect(calibrated.constraints.minSpecial).toEqual(Zero); }); }); }); diff --git a/libs/tools/generator/core/src/policies/dynamic-password-policy-constraints.ts b/libs/tools/generator/core/src/policies/dynamic-password-policy-constraints.ts index daff988254..7fe7606188 100644 --- a/libs/tools/generator/core/src/policies/dynamic-password-policy-constraints.ts +++ b/libs/tools/generator/core/src/policies/dynamic-password-policy-constraints.ts @@ -7,7 +7,7 @@ import { import { DefaultPasswordBoundaries } from "../data"; import { PasswordGeneratorPolicy, PasswordGeneratorSettings } from "../types"; -import { atLeast, atLeastSum, maybe, readonlyTrueWhen, AtLeastOne } from "./constraints"; +import { atLeast, atLeastSum, maybe, readonlyTrueWhen, AtLeastOne, Zero } from "./constraints"; import { PasswordPolicyConstraints } from "./password-policy-constraints"; /** Creates state constraints by blending policy and password settings. */ @@ -68,8 +68,8 @@ export class DynamicPasswordPolicyConstraints ...this.constraints, minLowercase: maybe(lowercase, this.constraints.minLowercase ?? AtLeastOne), minUppercase: maybe(uppercase, this.constraints.minUppercase ?? AtLeastOne), - minNumber: maybe(number, this.constraints.minNumber), - minSpecial: maybe(special, this.constraints.minSpecial), + minNumber: maybe(number, this.constraints.minNumber) ?? Zero, + minSpecial: maybe(special, this.constraints.minSpecial) ?? Zero, }; // lower bound of length must always at least fit its sub-lengths From ec92f8250199e0f34e33b8bbf37f3abdb5bf5950 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Thu, 7 Nov 2024 21:28:40 +0100 Subject: [PATCH 11/47] Fix wrong import of SendFormModule (#11893) Co-authored-by: Daniel James Smith --- .../src/tools/popup/send-v2/add-edit/send-add-edit.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 585f6067e3..d100588365 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"; From f9098558a6f3b765545a80752770ba4a4491a903 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:00:48 -0500 Subject: [PATCH 12/47] Update Is-Prerelease header to be integer instead of string (#11909) --- libs/common/src/services/api.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index f9e05e7635..0c508bfeb8 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -1886,7 +1886,7 @@ export class ApiService implements ApiServiceAbstraction { }); if (flagEnabled("prereleaseBuild")) { - headers.set("Is-Prerelease", "true"); + headers.set("Is-Prerelease", "1"); } if (this.customUserAgent != null) { headers.set("User-Agent", this.customUserAgent); From f206e0f817a19d12bd8db7f9a9466c7adc809498 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:41:47 -0500 Subject: [PATCH 13/47] Move Packages to Platform & KM (#11907) --- .github/renovate.json | 50 ++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index c9cfd54895..0172403f0f 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"] From e8dac0cc122d35d6a279fd64adda10b2df97e402 Mon Sep 17 00:00:00 2001 From: Victoria League Date: Thu, 7 Nov 2024 16:54:49 -0500 Subject: [PATCH 14/47] [CL-500] Add disclosure component and directive (#11865) --- .../disclosure-trigger-for.directive.ts | 27 +++++++++ .../src/disclosure/disclosure.component.ts | 21 +++++++ libs/components/src/disclosure/disclosure.mdx | 55 +++++++++++++++++++ .../src/disclosure/disclosure.stories.ts | 29 ++++++++++ libs/components/src/disclosure/index.ts | 2 + .../src/icon-button/icon-button.component.ts | 4 ++ .../src/icon-button/icon-button.mdx | 20 +++---- .../src/icon-button/icon-button.stories.ts | 8 +-- libs/components/src/index.ts | 1 + 9 files changed, 152 insertions(+), 15 deletions(-) create mode 100644 libs/components/src/disclosure/disclosure-trigger-for.directive.ts create mode 100644 libs/components/src/disclosure/disclosure.component.ts create mode 100644 libs/components/src/disclosure/disclosure.mdx create mode 100644 libs/components/src/disclosure/disclosure.stories.ts create mode 100644 libs/components/src/disclosure/index.ts diff --git a/libs/components/src/disclosure/disclosure-trigger-for.directive.ts b/libs/components/src/disclosure/disclosure-trigger-for.directive.ts new file mode 100644 index 0000000000..0547028172 --- /dev/null +++ b/libs/components/src/disclosure/disclosure-trigger-for.directive.ts @@ -0,0 +1,27 @@ +import { Directive, HostBinding, HostListener, Input } from "@angular/core"; + +import { DisclosureComponent } from "./disclosure.component"; + +@Directive({ + selector: "[bitDisclosureTriggerFor]", + exportAs: "disclosureTriggerFor", + standalone: true, +}) +export class DisclosureTriggerForDirective { + /** + * Accepts template reference for a bit-disclosure component instance + */ + @Input("bitDisclosureTriggerFor") disclosure: DisclosureComponent; + + @HostBinding("attr.aria-expanded") get ariaExpanded() { + return this.disclosure.open; + } + + @HostBinding("attr.aria-controls") get ariaControls() { + return this.disclosure.id; + } + + @HostListener("click") click() { + this.disclosure.open = !this.disclosure.open; + } +} diff --git a/libs/components/src/disclosure/disclosure.component.ts b/libs/components/src/disclosure/disclosure.component.ts new file mode 100644 index 0000000000..58c67ad0f0 --- /dev/null +++ b/libs/components/src/disclosure/disclosure.component.ts @@ -0,0 +1,21 @@ +import { Component, HostBinding, Input, booleanAttribute } from "@angular/core"; + +let nextId = 0; + +@Component({ + selector: "bit-disclosure", + standalone: true, + template: ``, +}) +export class DisclosureComponent { + /** + * Optionally init the disclosure in its opened state + */ + @Input({ transform: booleanAttribute }) open?: boolean = false; + + @HostBinding("class") get classList() { + return this.open ? "" : "tw-hidden"; + } + + @HostBinding("id") id = `bit-disclosure-${nextId++}`; +} diff --git a/libs/components/src/disclosure/disclosure.mdx b/libs/components/src/disclosure/disclosure.mdx new file mode 100644 index 0000000000..8df8e7025b --- /dev/null +++ b/libs/components/src/disclosure/disclosure.mdx @@ -0,0 +1,55 @@ +import { Meta, Story, Primary, Controls } from "@storybook/addon-docs"; + +import * as stories from "./disclosure.stories"; + + + +```ts +import { DisclosureComponent, DisclosureTriggerForDirective } from "@bitwarden/components"; +``` + +# Disclosure + +The `bit-disclosure` component is used in tandem with the `bitDisclosureTriggerFor` directive to +create an accessible content area whose visibility is controlled by a trigger button. + +To compose a disclosure and trigger: + +1. Create a trigger component (see "Supported Trigger Components" section below) +2. Create a `bit-disclosure` +3. Set a template reference on the `bit-disclosure` +4. Use the `bitDisclosureTriggerFor` directive on the trigger component, and pass it the + `bit-disclosure` template reference +5. Set the `open` property on the `bit-disclosure` to init the disclosure as either currently + expanded or currently collapsed. The disclosure will default to `false`, meaning it defaults to + being hidden. + +``` + +click button to hide this content +``` + + + +
+
+ +## Supported Trigger Components + +This is the list of currently supported trigger components: + +- Icon button `muted` variant + +## Accessibility + +The disclosure and trigger directive functionality follow the +[Disclosure (Show/Hide)](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/) pattern for +accessibility, automatically handling the `aria-controls` and `aria-expanded` properties. A `button` +element must be used as the trigger for the disclosure. The `button` element must also have an +accessible label/title -- please follow the accessibility guidelines for whatever trigger component +you choose. diff --git a/libs/components/src/disclosure/disclosure.stories.ts b/libs/components/src/disclosure/disclosure.stories.ts new file mode 100644 index 0000000000..974589a667 --- /dev/null +++ b/libs/components/src/disclosure/disclosure.stories.ts @@ -0,0 +1,29 @@ +import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; + +import { IconButtonModule } from "../icon-button"; + +import { DisclosureTriggerForDirective } from "./disclosure-trigger-for.directive"; +import { DisclosureComponent } from "./disclosure.component"; + +export default { + title: "Component Library/Disclosure", + component: DisclosureComponent, + decorators: [ + moduleMetadata({ + imports: [DisclosureTriggerForDirective, DisclosureComponent, IconButtonModule], + }), + ], +} as Meta; + +type Story = StoryObj; + +export const DisclosureWithIconButton: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + click button to hide this content + `, + }), +}; diff --git a/libs/components/src/disclosure/index.ts b/libs/components/src/disclosure/index.ts new file mode 100644 index 0000000000..b5bdf68725 --- /dev/null +++ b/libs/components/src/disclosure/index.ts @@ -0,0 +1,2 @@ +export * from "./disclosure-trigger-for.directive"; +export * from "./disclosure.component"; diff --git a/libs/components/src/icon-button/icon-button.component.ts b/libs/components/src/icon-button/icon-button.component.ts index 54f6dfda96..d036e1c77c 100644 --- a/libs/components/src/icon-button/icon-button.component.ts +++ b/libs/components/src/icon-button/icon-button.component.ts @@ -52,10 +52,14 @@ const styles: Record = { "tw-bg-transparent", "!tw-text-muted", "tw-border-transparent", + "aria-expanded:tw-bg-text-muted", + "aria-expanded:!tw-text-contrast", "hover:tw-bg-transparent-hover", "hover:tw-border-primary-700", "focus-visible:before:tw-ring-primary-700", "disabled:tw-opacity-60", + "aria-expanded:hover:tw-bg-secondary-700", + "aria-expanded:hover:tw-border-secondary-700", "disabled:hover:tw-border-transparent", "disabled:hover:tw-bg-transparent", ...focusRing, diff --git a/libs/components/src/icon-button/icon-button.mdx b/libs/components/src/icon-button/icon-button.mdx index 8361d4c399..a45160d788 100644 --- a/libs/components/src/icon-button/icon-button.mdx +++ b/libs/components/src/icon-button/icon-button.mdx @@ -29,8 +29,6 @@ Icon buttons can be found in other components such as: the [dialog](?path=/docs/component-library-dialogs--docs), and [table](?path=/docs/component-library-table--docs). - - ## Styles There are 4 common styles for button main, muted, contrast, and danger. The other styles follow the @@ -40,48 +38,48 @@ button component styles. Used for general icon buttons appearing on the theme’s main `background` - + ### Muted Used for low emphasis icon buttons appearing on the theme’s main `background` - + ### Contrast Used on a theme’s colored or contrasting backgrounds such as in the navigation or on toasts and banners. - + ### Danger Danger is used for “trash” actions throughout the experience, most commonly in the bottom right of the dialog component. - + ### Primary Used in place of the main button component if no text is used. This allows the button to display square. - + ### Secondary Used in place of the main button component if no text is used. This allows the button to display square. - + ### Light Used on a background that is dark in both light theme and dark theme. Example: end user navigation styles. - + **Note:** Main and contrast styles appear on backgrounds where using `primary-700` as a focus indicator does not meet WCAG graphic contrast guidelines. @@ -95,11 +93,11 @@ with less padding around the icon, such as in the navigation component. ### Small - + ### Default - + ## Accessibility diff --git a/libs/components/src/icon-button/icon-button.stories.ts b/libs/components/src/icon-button/icon-button.stories.ts index 0f25d2de58..b5542f7860 100644 --- a/libs/components/src/icon-button/icon-button.stories.ts +++ b/libs/components/src/icon-button/icon-button.stories.ts @@ -23,7 +23,7 @@ type Story = StoryObj; export const Default: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ `
@@ -56,7 +56,7 @@ export const Small: Story = { export const Primary: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ ` `, }), @@ -96,7 +96,7 @@ export const Muted: Story = { export const Light: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ `
@@ -110,7 +110,7 @@ export const Light: Story = { export const Contrast: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ `
diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 6881d801e0..810f32bdd3 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -13,6 +13,7 @@ export * from "./chip-select"; export * from "./color-password"; export * from "./container"; export * from "./dialog"; +export * from "./disclosure"; export * from "./form-field"; export * from "./icon-button"; export * from "./icon"; From b23d62a28014b39f0bf6f2f7846145c757a07113 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Fri, 8 Nov 2024 00:28:32 +0100 Subject: [PATCH 15/47] [PM-14183] Show success toast on copy history value (#11911) * Remove unnecessary copy icon * Fix styling of copy success toast - Use CopyClick directive instead of manually copying and showing the toast - Adjust tests --------- Co-authored-by: Daniel James Smith --- .../password-history-view.component.html | 8 +++--- .../password-history-view.component.spec.ts | 24 ++---------------- .../password-history-view.component.ts | 25 ++----------------- 3 files changed, 8 insertions(+), 49 deletions(-) diff --git a/libs/vault/src/components/password-history-view/password-history-view.component.html b/libs/vault/src/components/password-history-view/password-history-view.component.html index 44b7fea5f7..459c679945 100644 --- a/libs/vault/src/components/password-history-view/password-history-view.component.html +++ b/libs/vault/src/components/password-history-view/password-history-view.component.html @@ -15,10 +15,10 @@ bitIconButton="bwi-clone" [appA11yTitle]="'copyPassword' | i18n" appStopClick - (click)="copy(h.password)" - > - - + [appCopyClick]="h.password" + [valueLabel]="'password' | i18n" + showToast + > diff --git a/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts b/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts index 8772a24582..3900681f23 100644 --- a/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts +++ b/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts @@ -3,14 +3,13 @@ import { By } from "@angular/platform-browser"; import { BehaviorSubject } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { ColorPasswordModule, ItemModule, ToastService } from "@bitwarden/components"; +import { ColorPasswordModule, ItemModule } from "@bitwarden/components"; import { ColorPasswordComponent } from "@bitwarden/components/src/color-password/color-password.component"; import { PasswordHistoryViewComponent } from "./password-history-view.component"; @@ -25,8 +24,6 @@ describe("PasswordHistoryViewComponent", () => { organizationId: "222-444-555", } as CipherView; - const copyToClipboard = jest.fn(); - const showToast = jest.fn(); const activeAccount$ = new BehaviorSubject<{ id: string }>({ id: "666-444-444" }); const mockCipherService = { get: jest.fn().mockResolvedValue({ decrypt: jest.fn().mockResolvedValue(mockCipher) }), @@ -36,17 +33,13 @@ describe("PasswordHistoryViewComponent", () => { beforeEach(async () => { mockCipherService.get.mockClear(); mockCipherService.getKeyForCipherKeyDecryption.mockClear(); - copyToClipboard.mockClear(); - showToast.mockClear(); await TestBed.configureTestingModule({ imports: [ItemModule, ColorPasswordModule, JslibModule], providers: [ - { provide: WINDOW, useValue: window }, { provide: CipherService, useValue: mockCipherService }, - { provide: PlatformUtilsService, useValue: { copyToClipboard } }, + { provide: PlatformUtilsService }, { provide: AccountService, useValue: { activeAccount$ } }, - { provide: ToastService, useValue: { showToast } }, { provide: I18nService, useValue: { t: (key: string) => key } }, ], }).compileComponents(); @@ -80,18 +73,5 @@ describe("PasswordHistoryViewComponent", () => { "bad-password-2", ]); }); - - it("copies a password", () => { - const copyButton = fixture.debugElement.query(By.css("button")); - - copyButton.nativeElement.click(); - - expect(copyToClipboard).toHaveBeenCalledWith("bad-password-1", { window: window }); - expect(showToast).toHaveBeenCalledWith({ - message: "passwordCopied", - title: "", - variant: "info", - }); - }); }); }); diff --git a/libs/vault/src/components/password-history-view/password-history-view.component.ts b/libs/vault/src/components/password-history-view/password-history-view.component.ts index 5e858af727..a0f0aa6b35 100644 --- a/libs/vault/src/components/password-history-view/password-history-view.component.ts +++ b/libs/vault/src/components/password-history-view/password-history-view.component.ts @@ -1,21 +1,14 @@ import { CommonModule } from "@angular/common"; -import { OnInit, Inject, Component, Input } from "@angular/core"; +import { OnInit, Component, Input } from "@angular/core"; import { firstValueFrom, map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { PasswordHistoryView } from "@bitwarden/common/vault/models/view/password-history.view"; -import { - ToastService, - ItemModule, - ColorPasswordModule, - IconButtonModule, -} from "@bitwarden/components"; +import { ItemModule, ColorPasswordModule, IconButtonModule } from "@bitwarden/components"; @Component({ selector: "vault-password-history-view", @@ -33,29 +26,15 @@ export class PasswordHistoryViewComponent implements OnInit { history: PasswordHistoryView[] = []; constructor( - @Inject(WINDOW) private win: Window, protected cipherService: CipherService, - protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, protected accountService: AccountService, - protected toastService: ToastService, ) {} async ngOnInit() { await this.init(); } - /** Copies a password to the clipboard. */ - copy(password: string) { - const copyOptions = this.win != null ? { window: this.win } : undefined; - this.platformUtilsService.copyToClipboard(password, copyOptions); - this.toastService.showToast({ - variant: "info", - title: "", - message: this.i18nService.t("passwordCopied"), - }); - } - /** Retrieve the password history for the given cipher */ protected async init() { const cipher = await this.cipherService.get(this.cipherId); From d40c1bad73a8ae154ee682b00e031a16c9ca35a8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 00:30:18 +0000 Subject: [PATCH 16/47] Autosync the updated translations (#11915) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/de/messages.json | 6 +++--- apps/desktop/src/locales/fi/messages.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 6e7282b9a8..070da06de0 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -1684,10 +1684,10 @@ "message": "Die Kontolöschung ist dauerhaft. Sie kann nicht rückgängig gemacht werden." }, "cannotDeleteAccount": { - "message": "Cannot delete account" + "message": "Konto kann nicht gelöscht werden" }, "cannotDeleteAccountDesc": { - "message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details." + "message": "Diese Aktion kann nicht abgeschlossen werden, da dein Konto im Besitz einer Organisation ist. Kontaktiere deinen Organisationsadministrator für weitere Details." }, "accountDeleted": { "message": "Konto gelöscht" @@ -2394,7 +2394,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/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index 063a63b1ac..cf6025e042 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -1800,7 +1800,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", From b4428160ca2e665c8bf7b81fead8acb16b91ed6b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 00:32:02 +0000 Subject: [PATCH 17/47] Autosync the updated translations (#11916) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/de/messages.json | 2 +- apps/browser/src/_locales/es/messages.json | 50 +++++++++---------- apps/browser/src/_locales/fi/messages.json | 12 ++--- apps/browser/src/_locales/sv/messages.json | 12 ++--- apps/browser/src/_locales/zh_TW/messages.json | 14 +++--- 5 files changed, 45 insertions(+), 45 deletions(-) diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 0686063d56..df664cf8d3 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/es/messages.json b/apps/browser/src/_locales/es/messages.json index cbb1475c4d..42e56a56ba 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 96a10c167a..054362a4a2 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 c05410956f..e58a262d59 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 76d2526292..26bbbd0054 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": { From d69642e7a02066d612b2df8a5b0dd055e0c76246 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:39:15 -0800 Subject: [PATCH 18/47] [PM-14468] - add feature flag for critical apps (#11871) * rename acess intelligence to risk insights * keep branch name * replace all instances of AccessIntelligence. strip raw data + members to just the table * revert change to feature flag name * add feature flag for critical apps * change flag name * Update libs/common/src/enums/feature-flag.enum.ts Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --------- Co-authored-by: Daniel James Smith Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- .../risk-insights/all-applications.component.html | 5 +++-- .../risk-insights/all-applications.component.ts | 10 +++++++++- .../risk-insights/risk-insights.component.html | 2 +- .../tools/risk-insights/risk-insights.component.ts | 14 ++++++++++++-- libs/common/src/enums/feature-flag.enum.ts | 2 ++ 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/apps/web/src/app/tools/risk-insights/all-applications.component.html b/apps/web/src/app/tools/risk-insights/all-applications.component.html index 5dfaa20240..4ed31adea7 100644 --- a/apps/web/src/app/tools/risk-insights/all-applications.component.html +++ b/apps/web/src/app/tools/risk-insights/all-applications.component.html @@ -57,6 +57,7 @@ type="button" buttonType="secondary" bitButton + *ngIf="isCritialAppsFeatureEnabled" [disabled]="!selectedIds.size" [loading]="markingAsCritical" (click)="markAppsAsCritical()" @@ -68,7 +69,7 @@ - + {{ "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 43d6da70e9..1c6a36b445 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/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index ea016e3435..2679a01b9f 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -35,6 +35,7 @@ export enum FeatureFlag { AccessIntelligence = "pm-13227-access-intelligence", Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions", LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split", + CriticalApps = "pm-14466-risk-insights-critical-application", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -80,6 +81,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.AccessIntelligence]: FALSE, [FeatureFlag.Pm13322AddPolicyDefinitions]: FALSE, [FeatureFlag.LimitCollectionCreationDeletionSplit]: FALSE, + [FeatureFlag.CriticalApps]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; From 2c914def29ce017cfc4286028c9c0189de6bc6d3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 10:02:31 +0100 Subject: [PATCH 19/47] [deps] Platform: Update macOS/iOS bindings (#11793) * [deps] Platform: Update macOS/iOS bindings * fix: update security-framework to support core-foundation 0.10.0 --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Andreas Coroiu --- apps/desktop/desktop_native/Cargo.lock | 12 ++++++------ apps/desktop/desktop_native/core/Cargo.toml | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index d8a7c470eb..74c75d38e7 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -388,9 +388,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", @@ -1667,9 +1667,9 @@ checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" [[package]] name = "security-framework" -version = "2.11.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "f9d0283c0a4a22a0f1b0e4edca251aa20b92fc96eaa09b84bec052f9415e9d71" dependencies = [ "bitflags", "core-foundation", @@ -1680,9 +1680,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", diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index a56c1b5743..4196e415e7 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -61,9 +61,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 } From 081fe83d83860a6ba47b921bcb660eee3490f519 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 8 Nov 2024 11:01:31 +0100 Subject: [PATCH 20/47] PM-10393 SSH keys (#10825) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [PM-10395] Add new item type ssh key (#10360) * Implement ssh-key cipher type * Fix linting * Fix edit and view components for ssh-keys on desktop * Fix tests * Remove ssh key type references * Remove add ssh key option * Fix typo * Add tests * [PM-10399] Add ssh key import export for bitwarden json (#10529) * Add ssh key import export for bitwarden json * Remove key type from ssh key export * [PM-10406] Add privatekey publickey and fingerprint to both add-edit and view co… (#11046) * Add privatekey publickey and fingerprint to both add-edit and view components * Remove wrong a11y title * Fix testid * [PM-10098] SSH Agent & SSH Key creation for Bitwarden Desktop (#10293) * Add ssh agent, generator & import * Move ssh agent code to bitwarden-russh crate * Remove generator component * Cleanup * Cleanup * Remove left over sshGenerator reference * Cleanup * Add documentation to sshkeyimportstatus * Fix outdated variable name * Update apps/desktop/src/platform/preload.ts Co-authored-by: Andreas Coroiu * Rename renderersshagent * Rename MainSshAgentService * Improve clarity of 'id' variables being used * Improve clarity of 'id' variables being used * Update apps/desktop/src/vault/app/vault/add-edit.component.html Co-authored-by: Andreas Coroiu * Fix outdated cipher/messageid names * Rename SSH to Ssh * Make agent syncing more reactive * Move constants to top of class * Make sshkey cipher filtering clearer * Add stricter equality check on ssh key unlock * Fix build and messages * Fix incorrect featureflag name * Replace anonymous async function with switchmap pipe * Fix build * Update apps/desktop/desktop_native/napi/src/lib.rs Co-authored-by: Andreas Coroiu * Revert incorrectly renamed 'Ssh' usages to SSH * Run cargo fmt * Clean up ssh agent sock path logic * Cleanup and split to platform specific files * Small cleanup * Pull out generator and importer into core * Rename renderersshagentservice to sshagentservice * Rename cipheruuid to cipher_id * Drop ssh dependencies from napi crate * Clean up windows build * Small cleanup * Small cleanup * Cleanup * Add rxjs pipeline for agent services * [PM-12555] Pkcs8 sshkey import & general ssh key import tests (#11048) * Add pkcs8 import and tests * Add key type unsupported error * Remove unsupported formats * Remove code for unsupported formats * Fix encrypted pkcs8 import * Add ed25519 pkcs8 unencrypted test file * SSH agent rxjs tweaks (#11148) * feat: rewrite sshagent.signrequest as purely observable * feat: fail the request when unlock times out * chore: clean up, add some clarifying comments * chore: remove unused dependency * fix: result `undefined` crashing in NAPI -> Rust * Allow concurrent SSH requests in rust * Remove unwraps * Cleanup and add init service init call * Fix windows * Fix timeout behavior on locked vault --------- Co-authored-by: Andreas Coroiu * Fix libc dependency being duplicated * fix SSH casing (#11840) * Move ssh agent behind feature flag (#11841) * Move ssh agent behind feature flag * Add separate flag for ssh agent * [PM-14215] fix unsupported key type error message (#11788) * Fix error message for import of unsupported ssh keys * Use triple equals in add-edit component for ssh keys --------- Co-authored-by: Andreas Coroiu Co-authored-by: aj-bw <81774843+aj-bw@users.noreply.github.com> --- .github/CODEOWNERS | 3 + apps/browser/src/_locales/en/messages.json | 36 + .../add-edit/add-edit-v2.component.ts | 2 + .../item-copy-actions.component.html | 24 + .../item-copy-actions.component.ts | 8 + .../vault-v2/view-v2/view-v2.component.ts | 2 + .../components/vault/add-edit.component.html | 20 + .../vault/vault-filter.component.html | 13 + .../components/vault/vault-items.component.ts | 3 + .../components/vault/view.component.html | 33 + .../vault-popup-items.service.spec.ts | 1 + .../services/vault-popup-items.service.ts | 1 + .../vault-popup-list-filters.service.spec.ts | 1 + .../vault-popup-list-filters.service.ts | 5 + apps/desktop/desktop_native/Cargo.lock | 637 +++++++++++++++++- apps/desktop/desktop_native/core/Cargo.toml | 24 +- .../desktop_native/core/src/biometric/mod.rs | 5 +- .../desktop_native/core/src/biometric/unix.rs | 22 +- .../core/src/biometric/windows.rs | 1 - apps/desktop/desktop_native/core/src/lib.rs | 3 + .../desktop_native/core/src/password/unix.rs | 6 +- .../core/src/powermonitor/linux.rs | 12 +- .../core/src/process_isolation/linux.rs | 14 +- .../core/src/ssh_agent/generator.rs | 45 ++ .../core/src/ssh_agent/importer.rs | 395 +++++++++++ .../desktop_native/core/src/ssh_agent/mod.rs | 118 ++++ .../ssh_agent/named_pipe_listener_stream.rs | 60 ++ .../test_keys/ecdsa_openssh_unencrypted | 8 + .../test_keys/ecdsa_openssh_unencrypted.pub | 1 + .../test_keys/ed25519_openssh_encrypted | 8 + .../test_keys/ed25519_openssh_encrypted.pub | 1 + .../test_keys/ed25519_openssh_unencrypted | 7 + .../test_keys/ed25519_openssh_unencrypted.pub | 1 + .../test_keys/ed25519_pkcs8_unencrypted | 4 + .../test_keys/ed25519_pkcs8_unencrypted.pub | 1 + .../ssh_agent/test_keys/rsa_openssh_encrypted | 39 ++ .../test_keys/rsa_openssh_encrypted.pub | 1 + .../test_keys/rsa_openssh_unencrypted | 38 ++ .../test_keys/rsa_openssh_unencrypted.pub | 1 + .../ssh_agent/test_keys/rsa_pkcs8_encrypted | 42 ++ .../test_keys/rsa_pkcs8_encrypted.pub | 1 + .../ssh_agent/test_keys/rsa_pkcs8_unencrypted | 40 ++ .../test_keys/rsa_pkcs8_unencrypted.pub | 1 + .../desktop_native/core/src/ssh_agent/unix.rs | 77 +++ .../core/src/ssh_agent/windows.rs | 41 ++ apps/desktop/desktop_native/napi/Cargo.toml | 7 +- apps/desktop/desktop_native/napi/index.d.ts | 35 + apps/desktop/desktop_native/napi/src/lib.rs | 213 +++++- .../src/app/accounts/settings.component.html | 17 + .../src/app/accounts/settings.component.ts | 13 + apps/desktop/src/app/app.module.ts | 2 + apps/desktop/src/app/services/init.service.ts | 3 + apps/desktop/src/locales/en/messages.json | 81 +++ apps/desktop/src/main.ts | 11 +- apps/desktop/src/main/window.main.ts | 13 + .../components/approve-ssh-request.html | 17 + .../components/approve-ssh-request.ts | 59 ++ .../platform/main/main-ssh-agent.service.ts | 115 ++++ apps/desktop/src/platform/preload.ts | 28 + .../services/desktop-settings.service.ts | 15 + .../platform/services/ssh-agent.service.ts | 183 +++++ .../vault/app/vault/add-edit.component.html | 111 ++- .../src/vault/app/vault/add-edit.component.ts | 72 +- .../filters/type-filter.component.html | 15 + .../src/vault/app/vault/view.component.html | 99 +++ .../individual-vault/add-edit.component.html | 93 +++ .../components/vault-filter.component.ts | 6 + apps/web/src/locales/en/messages.json | 27 + .../vault/components/add-edit.component.ts | 16 +- .../src/vault/components/view.component.ts | 5 + libs/common/src/enums/feature-flag.enum.ts | 4 + .../common/src/models/export/cipher.export.ts | 11 + .../src/models/export/ssh-key.export.ts | 44 ++ libs/common/src/vault/enums/cipher-type.ts | 1 + .../src/vault/icon/build-cipher-icon.ts | 3 + .../src/vault/models/api/ssh-key.api.ts | 17 + .../src/vault/models/data/cipher.data.ts | 5 + .../src/vault/models/data/ssh-key.data.ts | 17 + libs/common/src/vault/models/domain/cipher.ts | 14 + .../src/vault/models/domain/ssh-key.spec.ts | 67 ++ .../common/src/vault/models/domain/ssh-key.ts | 70 ++ .../vault/models/request/cipher.request.ts | 13 + .../vault/models/response/cipher.response.ts | 7 + .../src/vault/models/view/cipher.view.ts | 7 + .../src/vault/models/view/ssh-key.view.ts | 41 ++ .../src/vault/services/cipher.service.ts | 14 + .../dialog/import-success-dialog.component.ts | 7 + .../src/cipher-form/cipher-form-container.ts | 2 + .../components/cipher-form.component.html | 6 + .../components/cipher-form.component.ts | 2 + .../sshkey-section.component.html | 30 + .../sshkey-section.component.ts | 80 +++ .../cipher-view/cipher-view.component.html | 3 + .../src/cipher-view/cipher-view.component.ts | 6 + .../sshkey-view.component.html | 50 ++ .../sshkey-sections/sshkey-view.component.ts | 35 + .../components/copy-cipher-field.directive.ts | 6 + .../src/services/copy-cipher-field.service.ts | 8 +- 98 files changed, 3572 insertions(+), 60 deletions(-) create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/generator.rs create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/importer.rs create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/mod.rs create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ecdsa_openssh_unencrypted create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ecdsa_openssh_unencrypted.pub create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_encrypted create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_encrypted.pub create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_unencrypted create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_openssh_unencrypted.pub create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_pkcs8_unencrypted create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/test_keys/ed25519_pkcs8_unencrypted.pub create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_encrypted create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_encrypted.pub create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_unencrypted create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_unencrypted.pub create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_encrypted create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_encrypted.pub create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_unencrypted create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_unencrypted.pub create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/unix.rs create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/windows.rs create mode 100644 apps/desktop/src/platform/components/approve-ssh-request.html create mode 100644 apps/desktop/src/platform/components/approve-ssh-request.ts create mode 100644 apps/desktop/src/platform/main/main-ssh-agent.service.ts create mode 100644 apps/desktop/src/platform/services/ssh-agent.service.ts create mode 100644 libs/common/src/models/export/ssh-key.export.ts create mode 100644 libs/common/src/vault/models/api/ssh-key.api.ts create mode 100644 libs/common/src/vault/models/data/ssh-key.data.ts create mode 100644 libs/common/src/vault/models/domain/ssh-key.spec.ts create mode 100644 libs/common/src/vault/models/domain/ssh-key.ts create mode 100644 libs/common/src/vault/models/view/ssh-key.view.ts create mode 100644 libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.html create mode 100644 libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts create mode 100644 libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.html create mode 100644 libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5ba5885d72..93693f183c 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/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 92fabdae3e..a62dac0543 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/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 5febed788e..8d95acbce9 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 f4444a10ae..973b1f9f1a 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 a53c4a7c35..00a775024c 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/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index cc2c419c04..b1cbe8bc3e 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 32d7c283b2..fb1efbbbd7 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 f9ae340a89..f5c28b2beb 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 ee6858fe44..27d36cbc2f 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 73415c9070..57a5d007d8 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 42d76e1dfe..610d48fdc6 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 09c7d5fb0d..20ac3b3de9 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 5d7e690193..02ad7375f6 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 4059a43b56..590807cff6 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/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 74c75d38e7..7136384ff2 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" @@ -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" @@ -1027,6 +1262,15 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + [[package]] name = "libc" version = "0.2.159" @@ -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" @@ -1665,13 +2139,24 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "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", + "bitflags 2.6.0", "core-foundation", "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", @@ -1946,10 +2510,21 @@ dependencies = [ ] [[package]] -name = "tokio-util" -version = "0.7.12" +name = "tokio-stream" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", @@ -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 4196e415e7..cf409ccc29 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.159" +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" +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.11" thiserror = "=1.0.68" -tokio = { version = "=1.41.0", features = ["io-util", "sync", "macros"] } -tokio-util = "=0.7.12" 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 } diff --git a/apps/desktop/desktop_native/core/src/biometric/mod.rs b/apps/desktop/desktop_native/core/src/biometric/mod.rs index c41ad9dda5..72352cf228 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 742b736e81..563bd1dfe5 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 c5db9e3277..d5e8b6dc91 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 3132c56f7f..f38e6ef97b 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 53053ee467..1817a4d62e 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 fe07ad11ff..7d0fde15ed 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 ba8734cff7..dc027e0b54 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 0000000000..fe639f20e7 --- /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 0000000000..3d643e764c --- /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 0000000000..ad0ac837af --- /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 0000000000..69399ae753 --- /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 0000000000..9cf518f8af --- /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 0000000000..75e08b88b2 --- /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 0000000000..d3244a3d94 --- /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 0000000000..1188fa43f1 --- /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 0000000000..08184f3184 --- /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 0000000000..5c39882202 --- /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 0000000000..09eb728601 --- /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 0000000000..40997e18c8 --- /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 0000000000..bb7bbd85cf --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_encrypted @@ -0,0 +1,39 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABApatKZWf +0kXnaSVhty/RaKAAAAGAAAAAEAAAGXAAAAB3NzaC1yc2EAAAADAQABAAABgQC/v18xGP3q +zRV9iWqyiuwHZ4GpC4K2NO2/i2Yv5A3/bnal7CmiMh/S78lphgxcWtFkwrwlb321FmdHBv +6KOW+EzSiPvmsdkkbpfBXB3Qf2SlhZOZZ7lYeu8KAxL3exvvn8O1GGlUjXGUrFgmC60tHW +DBc1Ncmo8a2dwDLmA/sbLa8su2dvYEFmRg1vaytLDpkn8GS7zAxrUl/g0W2RwkPsByduUz +iQuX90v9WAy7MqOlwBRq6t5o8wdDBVODe0VIXC7N1OS42YUsKF+N0XOnLiJrIIKkXpahMD +pKZHeHQAdUQzsJVhKoLJR8DNDTYyhnJoQG7Q6m2gDTca9oAWvsBiNoEwCvwrt7cDNCz/Gs +lH9HXQgfWcVXn8+fuZgvjO3CxUI16Ev33m0jWoOKJcgK/ZLRnk8SEvsJ8NO32MeR/qUb7I +N/yUcDmPMI/3ecQsakF2cwNzHkyiGVo//yVTpf+vk8b89L+GXbYU5rtswtc2ZEGsQnUkao +NqS8mHqhWQBUkAAAWArmugDAR1KlxY8c/esWbgQ4oP/pAQApehDcFYOrS9Zo78Os4ofEd1 +HkgM7VG1IJafCnn+q+2VXD645zCsx5UM5Y7TcjYDp7reM19Z9JCidSVilleRedTj6LTZx1 +SvetIrTfr81SP6ZGZxNiM0AfIZJO5vk+NliDdbUibvAuLp3oYbzMS3syuRkJePWu+KSxym +nm2+88Wku94p6SIfGRT3nQsMfLS9x6fGQP5Z71DM91V33WCVhrBnvHgNxuAzHDZNfzbPu9 +f2ZD1JGh8azDPe0XRD2jZTyd3Nt+uFMcwnMdigTXaTHExEFkTdQBea1YoprIG56iNZTSoU +/RwE4A0gdrSgJnh+6p8w05u+ia0N2WSL5ZT9QydPhwB8pGHuGBYoXFcAcFwCnIAExPtIUh +wLx1NfC/B2MuD3Uwbx96q5a7xMTH51v0eQDdY3mQzdq/8OHHn9vzmEfV6mxmuyoa0Vh+WG +l2WLB2vD5w0JwRAFx6a3m/rD7iQLDvK3UiYJ7DVz5G3/1w2m4QbXIPCfI3XHU12Pye2a0m +/+/wkS4/BchqB0T4PJm6xfEynXwkEolndf+EvuLSf53XSJ2tfeFPGmmCyPoy9JxCce7wVk +FB/SJw6LXSGUO0QA6vzxbzLEMNrqrpcCiUvDGTA6jds0HnSl8hhgMuZOtQDbFoovIHX0kl +I5pD5pqaUNvQ3+RDFV3qdZyDntaPwCNJumfqUy46GAhYVN2O4p0HxDTs4/c2rkv+fGnG/P +8wc7ACz3QNdjb7XMrW3/vNuwrh/sIjNYM2aiVWtRNPU8bbSmc1sYtpJZ5CsWK1TNrDrY6R +OV89NjBoEC5OXb1c75VdN/jSssvn72XIHjkkDEPboDfmPe889VHfsVoBm18uvWPB4lffdm +4yXAr+Cx16HeiINjcy6iKym2p4ED5IGaSXlmw/6fFgyh2iF7kZTnHawVPTqJNBVMaBRvHn +ylMBLhhEkrXqW43P4uD6l0gWCAPBczcSjHv3Yo28ExtI0QKNk/Uwd2q2kxFRWCtqUyQkrF +KG9IK+ixqstMo+xEb+jcCxCswpJitEIrDOXd51sd7PjCGZtAQ6ycpOuFfCIhwxlBUZdf2O +kM/oKqN/MKMDk+H/OVl8XrLalBOXYDllW+NsL8W6F8DMcdurpQ8lCJHHWBgOdNd62STdvZ +LBf7v8OIrC6F0bVGushsxb7cwGiUrjqUfWjhZoKx35V0dWBcGx7GvzARkvSUM22q14lc7+ +XTP0qC8tcRQfRbnBPJdmnbPDrJeJcDv2ZdbAPdzf2C7cLuuP3mNwLCrLUc7gcF/xgH+Xtd +6KOvzt2UuWv5+cqWOsNspG+lCY0P11BPhlMvmZKO8RGVGg7PKAatG4mSH4IgO4DN2t7U9B +j+v2jq2z5O8O4yJ8T2kWnBlhWzlBoL+R6aaat421f0v+tW/kEAouBQob5I0u1VLB2FkpZE +6tOCK47iuarhf/86NtlPfCM9PdWJQOKcYQ8DCQhp5Lvgd0Vj3WzY+BISDdB2omGRhLUly/ +i40YPASAVnWvgqpCQ4E3rs4DWI/kEcvQH8zVq2YoRa6fVrVf1w/GLFC7m/wkxw8fDfZgMS +Mu+ygbFa9H3aOSZMpTXhdssbOhU70fZOe6GWY9kLBNV4trQeb/pRdbEbMtEmN5TLESgwLA +43dVdHjvpZS677FN/d9+q+pr0Xnuc2VdlXkUyOyv1lFPJIN/XIotiDTnZ3epQQ1zQ3mx32 +8Op2EVgFWpwNmGXJ1zCCA6loUG7e4W/iXkKQxTvOM0fmE4a1Y387GDwJ+pZevYOIOYTkTa +l5jM/6Wm3pLNyE8Ynw3OX0T/p9TO1i3DlXXE/LzcWJFFXAQMo+kc+GlXqjP7K7c6xjQ6vx +2MmKBw== +-----END OPENSSH PRIVATE KEY----- diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_encrypted.pub b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_openssh_encrypted.pub new file mode 100644 index 0000000000..d37f573b68 --- /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 0000000000..0d2692e14a --- /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 0000000000..9ec8fec5c5 --- /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 0000000000..e84d1f07a3 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_encrypted @@ -0,0 +1,42 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIHdTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQXquAya5XFx11QEPm +KCSnlwICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEAQIEEKVtEIkI2ELppfUQ +IwfNzowEggcQtWhXVz3LunYTSRVgnexcHEaGkUF6l6a0mGaLSczl+jdCwbbBxibU +EvN7+WMQ44shOk3LyThg0Irl22/7FuovmYc3TSeoMQH4mTROKF+9793v0UMAIAYd +ZhTsexTGncCOt//bq6Fl+L+qPNEkY/OjS+wI9MbOn/Agbcr8/IFSOxuSixxoTKgq +4QR5Ra3USCLyfm+3BoGPMk3tbEjrwjvzx/eTaWzt6hdc0yX4ehtqExF8WAYB43DW +3Y1slA1T464/f1j4KXhoEXDTBOuvNvnbr7lhap8LERIGYGnQKv2m2Kw57Wultnoe +joEQ+vTl5n92HI77H8tbgSbTYuEQ2n9pDD7AAzYGBn15c4dYEEGJYdHnqfkEF+6F +EgPa+Xhj2qqk5nd1bzPSv6iX7XfAX2sRzfZfoaFETmR0ZKbs0aMsndC5wVvd3LpA +m86VUihQxDvU8F4gizrNYj4NaNRv4lrxBj7Kb6BO/qT3DB8Uqu43oyrvA90iMigi +EvuCViwwhwCpe+AxCqLGrzvIpiZCksTOtSPEvnMehw2WA3yd/n88Nis5zD4b65+q +Tx9Q0Qm1LIi1Bq+s60+W1HK3KfaLrJaoX3JARZoWfxurZwtj+cMlo5zK1Ha2HHqQ +kVn21tOcQU/Yljt3Db+CKZ5Tos/rPywxGnkeMABzJgyajPHkYaSgWZrOEueihfS1 +5eDtEMBehEyHfcUrL7XGnn4lOzwQHZIEFnVdV0YGaQY8Wz212IjeWxV09gM2OEP6 +PEDI3GSsqOnGkPrnson5tsIUcvpk9smy9AA9qVhNowzeWCWmsF8K9fn/O94tIzyN +2EK0tkf8oDVROlbEh/jDa2aAHqPGCXBEqq1CbZXQpNk4FlRzkjtxdzPNiXLf45xO +IjOTTzgaVYWiKZD9ymNjNPIaDCPB6c4LtUm86xUQzXdztBm1AOI3PrNI6nIHxWbF +bPeEkJMRiN7C9j5nQMgQRB67CeLhzvqUdyfrYhzc7HY479sKDt9Qn8R0wpFw0QSA +G1gpGyxFaBFSdIsil5K4IZYXxh7qTlOKzaqArTI0Dnuk8Y67z8zaxN5BkvOfBd+Q +SoDz6dzn7KIJrK4XP3IoNfs6EVT/tlMPRY3Y/Ug+5YYjRE497cMxW8jdf3ZwgWHQ +JubPH+0IpwNNZOOf4JXALULsDj0N7rJ1iZAY67b+7YMin3Pz0AGQhQdEdqnhaxPh +oMvL9xFewkyujwCmPj1oQi1Uj2tc1i4ZpxY0XmYn/FQiQH9/XLdIlOMSTwGx86bw +90e9VJHfCmflLOpENvv5xr2isNbn0aXNAOQ4drWJaYLselW2Y4N1iqBCWJKFyDGw +4DevhhamEvsrdoKgvnuzfvA44kQGmfTjCuMu7IR5zkxevONNrynKcHkoWATzgxSS +leXCxzc9VA0W7XUSMypHGPNHJCwYZvSWGx0qGI3VREUk2J7OeVjXCFNeHFc2Le3P +dAm+DqRiyPBVX+yW+i7rjZLyypLzmYo9CyhlohOxTeGa6iTxBUZfYGoc0eJNqfgN +/5hkoPFYGkcd/p41SKSg7akrJPRc+uftH0oVI0wVorGSVOvwXRn7QM+wFKlv3DQD +ysMP7cOKqMyhJsqeW74/iWEmhbFIDKexSd/KTQ6PirVlzj7148Fl++yxaZpnZ6MY +iyzifvLcT701GaewIwi9YR9f1BWUXYHTjK3sB3lLPyMbA4w9bRkylcKrbGf85q0E +LXPlfh+1C9JctczDCqr2iLRoc/5j23GeN8RWfUNpZuxjFv9sxkV4iG+UapIuOBIc +Os4//3w24XcTXYqBdX2Y7+238xq6/94+4hIhXAcMFc2Nr3CEAZCuKYChVL9CSA3v +4sZM4rbOz6kWTC2G3SAtkLSk7hCJ6HLXzrnDb4++g3JYJWLeaQ+4ZaxWuKymnehN +xumXCwCn0stmCjXYV/yM3TeVnMfBTIB13KAjbn0czGW00nj79rNJJzkOlp9tIPen +pUPRFPWjgLF+hVQrwqJ3HPmt6Rt6mKzZ4FEpBXMDjvlKabnFvBdl3gbNHSfxhGHi +FzG3phg1CiXaURQUAf21PV+djfBha7kDwMXnpgZ+PIyGDxRj61StV/NSlhg+8GrL +ccoDOkfpy2zn++rmAqA21rTEChFN5djdsJw45GqPKUPOAgxKBsvqpoMIqq/C2pHP +iMiBriZULV9l0tHn5MMcNQbYAmp4BsTo6maHByAVm1/7/VPQn6EieuGroYgSk2H7 +pnwM01IUfGGP3NKlq9EiiF1gz8acZ5v8+jkZM2pIzh8Trw0mtwBpnyiyXmpbR/RG +m/TTU/gNQ/94ZaNJ/shPoBwikWXvOm+0Z0ZAwu3xefTyENGhjmb5GXshEN/5WwCm +NNrtUPlkGkYJrnSCVM/lHtjShwbLw2w/1sag1uDuXwirxxYh9r7D6HQ= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_encrypted.pub b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_encrypted.pub new file mode 100644 index 0000000000..f3c1b15f0a --- /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 0000000000..0bfe2bc506 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_unencrypted @@ -0,0 +1,40 @@ +-----BEGIN PRIVATE KEY----- +MIIG/QIBADANBgkqhkiG9w0BAQEFAASCBucwggbjAgEAAoIBgQCn4+QiJojZ9mgc +9KYJIvDWGaz4qFhf0CButg6L8zEoHKwuiN+mqcEciCCOa9BNiJmm8NTTehZvrrgl +GG59zIbqYtDAHjVn+vtb49xPzIv+M651Yqj08lIbR9tEIHKCq7aH8GlDm8NgG9Ez +JGjlL7okQym4TH1MHl+s4mUyr/qb2unlZBDixAQsphU8iCLftukWCIkmQg4CSj1G +h3WbBlZ+EX5eW0EXuAw4XsSbBTWV9CHRowVIpYqPvEYSpHsoCjEcd988p19hpiGk +nA0J4z7JfUlNgyT/1chb8GCTDT+2DCBRApbsIg6TOBVS+PR6emAQ3eZzUW0+3/oR +M4ip0ujltQy8uU6gvYIAqx5wXGMThVpZcUgahKiSsVo/s4b84iMe4DG3W8jz4qi6 +yyNv0VedEzPUZ1lXd1GJFoy9uKNuSTe+1ksicAcluZN6LuNsPHcPxFCzOcmoNnVX +EKAXInt+ys//5CDVasroZSAHZnDjUD4oNsLI3VIOnGxgXrkwSH0CAwEAAQKCAYAA +2SDMf7OBHw1OGM9OQa1ZS4u+ktfQHhn31+FxbrhWGp+lDt8gYABVf6Y4dKN6rMtn +7D9gVSAlZCAn3Hx8aWAvcXHaspxe9YXiZDTh+Kd8EIXxBQn+TiDA5LH0dryABqmM +p20vYKtR7OS3lIIXfFBSrBMwdunKzLwmKwZLWq0SWf6vVbwpxRyR9CyByodF6Djm +ZK3QB2qQ3jqlL1HWXL0VnyArY7HLvUvfLLK4vMPqnsSH+FdHvhcEhwqMlWT44g+f +hqWtCJNnjDgLK3FPbI8Pz9TF8dWJvOmp5Q6iSBua1e9x2LizVuNSqiFc7ZTLeoG4 +nDj7T2BtqB0E1rNUDEN1aBo+UZmHJK7LrzfW/B+ssi2WwIpfxYa1lO6HFod5/YQi +XV1GunyH1chCsbvOFtXvAHASO4HTKlJNbWhRF1GXqnKpAaHDPCVuwp3eq6Yf0oLb +XrL3KFZ3jwWiWbpQXRVvpqzaJwZn3CN1yQgYS9j17a9wrPky+BoJxXjZ/oImWLEC +gcEA0lkLwiHvmTYFTCC7PN938Agk9/NQs5PQ18MRn9OJmyfSpYqf/gNp+Md7xUgt +F/MTif7uelp2J7DYf6fj9EYf9g4EuW+SQgFP4pfiJn1+zGFeTQq1ISvwjsA4E8ZS +t+GIumjZTg6YiL1/A79u4wm24swt7iqnVViOPtPGOM34S1tAamjZzq2eZDmAF6pA +fmuTMdinCMR1E1kNJYbxeqLiqQCXuwBBnHOOOJofN3AkvzjRUBB9udvniqYxH3PQ +cxPxAoHBAMxT5KwBhZhnJedYN87Kkcpl7xdMkpU8b+aXeZoNykCeoC+wgIQexnSW +mFk4HPkCNxvCWlbkOT1MHrTAKFnaOww23Ob+Vi6A9n0rozo9vtoJig114GB0gUqE +mtfLhO1P5AE8yzogE+ILHyp0BqXt8vGIfzpDnCkN+GKl8gOOMPrR4NAcLO+Rshc5 +nLs7BGB4SEi126Y6mSfp85m0++1QhWMz9HzqJEHCWKVcZYdCdEONP9js04EUnK33 +KtlJIWzZTQKBwAT0pBpGwmZRp35Lpx2gBitZhcVxrg0NBnaO2fNyAGPvZD8SLQLH +AdAiov/a23Uc/PDbWLL5Pp9gwzj+s5glrssVOXdE8aUscr1b5rARdNNL1/Tos6u8 +ZUZ3sNqGaZx7a8U4gyYboexWyo9EC1C+AdkGBm7+AkM4euFwC9N6xsa/t5zKK5d6 +76hc0m+8SxivYCBkgkrqlfeGuZCQxU+mVsC0it6U+va8ojUjLGkZ80OuCwBf4xZl +3+acU7vx9o8/gQKBwB7BrhU6MWrsc+cr/1KQaXum9mNyckomi82RFYvb8Yrilcg3 +8FBy9XqNRKeBa9MLw1HZYpHbzsXsVF7u4eQMloDTLVNUC5L6dKAI1owoyTa24uH9 +0WWTg/a8mTZMe1jhgrew+AJq27NV6z4PswR9GenDmyshDDudz7rBsflZCQRoXUfW +RelV7BHU6UPBsXn4ASF4xnRyM6WvcKy9coKZcUqqgm3fLM/9OizCCMJgfXHBrE+x +7nBqst746qlEedSRrQKBwQCVYwwKCHNlZxl0/NMkDJ+hp7/InHF6mz/3VO58iCb1 +9TLDVUC2dDGPXNYwWTT9PclefwV5HNBHcAfTzgB4dpQyNiDyV914HL7DFEGduoPn +wBYjeFre54v0YjjnskjJO7myircdbdX//i+7LMUw5aZZXCC8a5BD/rdV6IKJWJG5 +QBXbe5fVf1XwOjBTzlhIPIqhNFfSu+mFikp5BRwHGBqsKMju6inYmW6YADeY/SvO +QjDEB37RqGZxqyIx8V2ZYwU= +-----END PRIVATE KEY----- diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_unencrypted.pub b/apps/desktop/desktop_native/core/src/ssh_agent/test_keys/rsa_pkcs8_unencrypted.pub new file mode 100644 index 0000000000..a3e04eed46 --- /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 0000000000..c1a3950666 --- /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 0000000000..fd6d9dacb9 --- /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 64ab106e57..d5bdc1f432 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.11" +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 8e1c1381b5..6d1a7b8abb 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 face07f2f4..60a8326a8e 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/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index 76cf98b1b2..7336ce09dd 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 e34d9cbb41..a3a9c92915 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 381c06e8b6..c3dcd191df 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 c855224cf9..e6c20d2e89 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/vault/individual-vault/add-edit.component.html b/apps/web/src/app/vault/individual-vault/add-edit.component.html index 855b5dac48..01ac60fc7e 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 @@
    + + +
    +
    + +
    + +
    + + +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    +