diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cf656967da0..959c176c5cf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,10 +4,6 @@ # # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners -## Secrets Manager team files ## -bitwarden_license/bit-web/src/app/secrets-manager @bitwarden/team-secrets-manager-dev -apps/web/src/app/secrets-manager/ @bitwarden/team-secrets-manager-dev - ## Auth team files ## apps/browser/src/auth @bitwarden/team-auth-dev apps/cli/src/auth @bitwarden/team-auth-dev @@ -107,6 +103,8 @@ apps/web/src/app/layouts @bitwarden/team-design-system ## Desktop native module ## apps/desktop/desktop_native @bitwarden/team-platform-dev +apps/desktop/desktop_native/objc/src/native/autofill @bitwarden/team-autofill-dev +apps/desktop/desktop_native/core/src/autofill @bitwarden/team-autofill-dev ## Key management team files ## apps/desktop/src/key-management @bitwarden/team-key-management-dev diff --git a/.github/renovate.json b/.github/renovate.json index 446aa789ebf..7f3e7464fe3 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -10,7 +10,7 @@ }, { "matchManagers": ["github-actions"], - "commitMessagePrefix": "[deps] DevOps:" + "commitMessagePrefix": "[deps] BRE:" }, { "matchManagers": ["cargo"], @@ -39,6 +39,10 @@ "groupName": "macOS/iOS bindings", "matchPackageNames": ["core-foundation", "security-framework", "security-framework-sys"] }, + { + "groupName": "zbus", + "matchPackageNames": ["zbus", "zbus_polkit"] + }, { "matchPackageNames": [ "base64-loader", diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index dc15f841c2b..bc9bdec396a 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -170,7 +170,7 @@ jobs: - name: Set up environment run: | sudo apt-get update - sudo apt-get -y install pkg-config libxss-dev libsecret-1-dev rpm musl-dev musl-tools flatpak flatpak-builder + sudo apt-get -y install pkg-config libxss-dev rpm musl-dev musl-tools flatpak flatpak-builder - name: Set up Snap run: sudo snap install snapcraft --classic diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index c1646997201..a940ce289ff 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -138,7 +138,7 @@ jobs: - name: Set up environment run: | sudo apt-get update - sudo apt-get -y install pkg-config libxss-dev libsecret-1-dev rpm + sudo apt-get -y install pkg-config libxss-dev rpm - name: Set up Snap run: sudo snap install snapcraft --classic diff --git a/.storybook/main.ts b/.storybook/main.ts index 454da4377dc..b48a86ba2b2 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -57,6 +57,7 @@ const config: StorybookConfig = { return config; }, docs: {}, + staticDirs: ["../apps/web/src/images"], }; export default config; diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index ab1c6377e6a..39bc6ed9b86 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Enter the 6 digit verification code from your authenticator app." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Enter the 6 digit verification code that was emailed to $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 8493358aa8b..1939b3f5716 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -438,8 +438,8 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { const successful = await this.trySetupBiometrics(); this.form.controls.biometric.setValue(successful); + await this.biometricStateService.setBiometricUnlockEnabled(successful); if (!successful) { - await this.biometricStateService.setBiometricUnlockEnabled(false); await this.biometricStateService.setFingerprintValidated(false); } } else { diff --git a/apps/browser/src/auth/popup/update-temp-password.component.html b/apps/browser/src/auth/popup/update-temp-password.component.html index 6e0cc0f4483..0ce82aa20cf 100644 --- a/apps/browser/src/auth/popup/update-temp-password.component.html +++ b/apps/browser/src/auth/popup/update-temp-password.component.html @@ -1,7 +1,7 @@
- {{ "logOut" | i18n }} +

{{ "updateMasterPassword" | i18n }} diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index c42d1f7e640..7b80c436798 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -362,8 +362,6 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.inlineMenuFido2Credentials.clear(); this.storeInlineMenuFido2Credentials$.next(currentTab.id); - await this.generatePassword(); - const ciphersViews = await this.getCipherViews(currentTab, updateAllCipherTypes); for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) { this.inlineMenuCiphers.set(`inline-menu-cipher-${cipherIndex}`, ciphersViews[cipherIndex]); diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 77a720557c6..b158a83c566 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -6,6 +6,7 @@ import { EnvironmentSelectorRouteData, ExtensionDefaultOverlayPosition, } from "@bitwarden/angular/auth/components/environment-selector.component"; +import { TwoFactorTimeoutComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-expired.component"; import { unauthUiRefreshRedirect } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-redirect"; import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap"; import { @@ -38,6 +39,7 @@ import { VaultIcon, LoginDecryptionOptionsComponent, DevicesIcon, + TwoFactorTimeoutIcon, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -199,6 +201,29 @@ const routes: Routes = [ ], }, ), + { + path: "", + component: ExtensionAnonLayoutWrapperComponent, + children: [ + { + path: "2fa-timeout", + canActivate: [unauthGuardFn(unauthRouteOverrides)], + children: [ + { + path: "", + component: TwoFactorTimeoutComponent, + }, + ], + data: { + pageTitle: { + key: "authenticationTimeout", + }, + pageIcon: TwoFactorTimeoutIcon, + elevation: 1, + } satisfies RouteDataProperties & AnonLayoutWrapperData, + }, + ], + }, { path: "2fa-options", component: TwoFactorOptionsComponent, diff --git a/apps/browser/src/tools/popup/services/file-popout-utils.service.ts b/apps/browser/src/tools/popup/services/file-popout-utils.service.ts index 65a311e47c3..fa72e411316 100644 --- a/apps/browser/src/tools/popup/services/file-popout-utils.service.ts +++ b/apps/browser/src/tools/popup/services/file-popout-utils.service.ts @@ -49,7 +49,7 @@ export class FilePopoutUtilsService { } /** - * Determines whether to show a file popout callout message for Chromium-based browsers in Linux and Mac OS X Big Sur + * Determines whether to show a file popout callout message for Chromium-based browsers in Linux and Mac OS X * @param win - The window context in which the check should be performed. * @returns True if the extension is not in a sidebar or popout; otherwise, false. */ @@ -66,8 +66,6 @@ export class FilePopoutUtilsService { } private isUnsupportedMac(win: Window): boolean { - return ( - this.platformUtilsService.isChrome() && win?.navigator?.appVersion.includes("Mac OS X 11") - ); + return this.platformUtilsService.isChrome() && win?.navigator?.appVersion.includes("Mac OS X"); } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html index 78403784f46..7b31e647bf0 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html @@ -3,19 +3,27 @@ {{ "new" | i18n }} - + {{ "typeLogin" | i18n }} - + {{ "typeCard" | i18n }} - + {{ "typeIdentity" | i18n }} - + {{ "note" | i18n }} diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts index 6842f35ea6f..b5dc2a2f034 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts @@ -1,141 +1,163 @@ import { CommonModule } from "@angular/common"; import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { Router } from "@angular/router"; +import { ActivatedRoute, RouterLink } from "@angular/router"; +import { mock } from "jest-mock-extended"; -import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; +import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; -import { ButtonModule, DialogService, MenuModule } from "@bitwarden/components"; +import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components"; import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; -import { AddEditQueryParams } from "../add-edit/add-edit-v2.component"; -import { AddEditFolderDialogComponent } from "../add-edit-folder-dialog/add-edit-folder-dialog.component"; import { NewItemDropdownV2Component, NewItemInitialValues } from "./new-item-dropdown-v2.component"; describe("NewItemDropdownV2Component", () => { let component: NewItemDropdownV2Component; let fixture: ComponentFixture; - const open = jest.fn(); - const navigate = jest.fn(); + let dialogServiceMock: jest.Mocked; + let browserApiMock: jest.Mocked; - jest - .spyOn(BrowserApi, "getTabFromCurrentWindow") - .mockResolvedValue({ url: "https://example.com" } as chrome.tabs.Tab); + const mockTab = { url: "https://example.com" }; + + beforeAll(() => { + jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(mockTab as chrome.tabs.Tab); + jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false); + jest.spyOn(Utils, "getHostname").mockReturnValue("example.com"); + }); beforeEach(async () => { - open.mockClear(); - navigate.mockClear(); + dialogServiceMock = mock(); + dialogServiceMock.open.mockClear(); + + const activatedRouteMock = { + snapshot: { paramMap: { get: jest.fn() } }, + }; + + const i18nServiceMock = mock(); + const folderServiceMock = mock(); + const folderApiServiceAbstractionMock = mock(); + const accountServiceMock = mock(); await TestBed.configureTestingModule({ - imports: [NewItemDropdownV2Component, MenuModule, ButtonModule, JslibModule, CommonModule], - providers: [ - { provide: I18nService, useValue: { t: (key: string) => key } }, - { provide: Router, useValue: { navigate } }, + imports: [ + CommonModule, + RouterLink, + ButtonModule, + MenuModule, + NoItemsModule, + NewItemDropdownV2Component, ], - }) - .overrideProvider(DialogService, { useValue: { open } }) - .compileComponents(); + providers: [ + { provide: DialogService, useValue: dialogServiceMock }, + { provide: I18nService, useValue: i18nServiceMock }, + { provide: ActivatedRoute, useValue: activatedRouteMock }, + { provide: BrowserApi, useValue: browserApiMock }, + { provide: FolderService, useValue: folderServiceMock }, + { provide: FolderApiServiceAbstraction, useValue: folderApiServiceAbstractionMock }, + { provide: AccountService, useValue: accountServiceMock }, + ], + }).compileComponents(); + }); + beforeEach(() => { fixture = TestBed.createComponent(NewItemDropdownV2Component); component = fixture.componentInstance; fixture.detectChanges(); }); - it("opens new folder dialog", () => { - component.openFolderDialog(); - - expect(open).toHaveBeenCalledWith(AddEditFolderDialogComponent); - }); - - describe("new item", () => { - const emptyParams: AddEditQueryParams = { - collectionId: undefined, - organizationId: undefined, - folderId: undefined, - }; - - beforeEach(() => { - jest.spyOn(component, "newItemNavigate"); - }); - - it("navigates to new login", async () => { - await component.newItemNavigate(CipherType.Login); - - expect(navigate).toHaveBeenCalledWith(["/add-cipher"], { - queryParams: { - type: CipherType.Login.toString(), - name: "example.com", - uri: "https://example.com", - ...emptyParams, - }, - }); - }); - - it("navigates to new card", async () => { - await component.newItemNavigate(CipherType.Card); - - expect(navigate).toHaveBeenCalledWith(["/add-cipher"], { - queryParams: { type: CipherType.Card.toString(), ...emptyParams }, - }); - }); - - it("navigates to new identity", async () => { - await component.newItemNavigate(CipherType.Identity); - - expect(navigate).toHaveBeenCalledWith(["/add-cipher"], { - queryParams: { type: CipherType.Identity.toString(), ...emptyParams }, - }); - }); - - it("navigates to new note", async () => { - await component.newItemNavigate(CipherType.SecureNote); - - expect(navigate).toHaveBeenCalledWith(["/add-cipher"], { - queryParams: { type: CipherType.SecureNote.toString(), ...emptyParams }, - }); - }); - - it("includes initial values", async () => { + describe("buildQueryParams", () => { + it("should build query params for a Login cipher when not popped out", async () => { + await component.ngOnInit(); component.initialValues = { folderId: "222-333-444", organizationId: "444-555-666", collectionId: "777-888-999", } as NewItemInitialValues; - await component.newItemNavigate(CipherType.Login); + jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false); + jest.spyOn(Utils, "getHostname").mockReturnValue("example.com"); - expect(navigate).toHaveBeenCalledWith(["/add-cipher"], { - queryParams: { - type: CipherType.Login.toString(), - folderId: "222-333-444", - organizationId: "444-555-666", - collectionId: "777-888-999", - uri: "https://example.com", - name: "example.com", - }, + const params = component.buildQueryParams(CipherType.Login); + + expect(params).toEqual({ + type: CipherType.Login.toString(), + collectionId: "777-888-999", + organizationId: "444-555-666", + folderId: "222-333-444", + uri: "https://example.com", + name: "example.com", }); }); - it("does not include name or uri when the extension is popped out", async () => { + it("should build query params for a Login cipher when popped out", () => { + component.initialValues = { + collectionId: "777-888-999", + } as NewItemInitialValues; + jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(true); + const params = component.buildQueryParams(CipherType.Login); + + expect(params).toEqual({ + type: CipherType.Login.toString(), + collectionId: "777-888-999", + }); + }); + + it("should build query params for a secure note", () => { component.initialValues = { - folderId: "222-333-444", - organizationId: "444-555-666", collectionId: "777-888-999", } as NewItemInitialValues; - await component.newItemNavigate(CipherType.Login); + const params = component.buildQueryParams(CipherType.SecureNote); - expect(navigate).toHaveBeenCalledWith(["/add-cipher"], { - queryParams: { - type: CipherType.Login.toString(), - folderId: "222-333-444", - organizationId: "444-555-666", - collectionId: "777-888-999", - }, + expect(params).toEqual({ + type: CipherType.SecureNote.toString(), + collectionId: "777-888-999", + }); + }); + + it("should build query params for an Identity", () => { + component.initialValues = { + collectionId: "777-888-999", + } as NewItemInitialValues; + + const params = component.buildQueryParams(CipherType.Identity); + + expect(params).toEqual({ + type: CipherType.Identity.toString(), + collectionId: "777-888-999", + }); + }); + + it("should build query params for a Card", () => { + component.initialValues = { + collectionId: "777-888-999", + } as NewItemInitialValues; + + const params = component.buildQueryParams(CipherType.Card); + + expect(params).toEqual({ + type: CipherType.Card.toString(), + collectionId: "777-888-999", + }); + }); + + it("should build query params for a SshKey", () => { + component.initialValues = { + collectionId: "777-888-999", + } as NewItemInitialValues; + + const params = component.buildQueryParams(CipherType.SshKey); + + expect(params).toEqual({ + type: CipherType.SshKey.toString(), + collectionId: "777-888-999", }); }); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts index a1d5cbd332d..e2062101e1b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts @@ -1,6 +1,6 @@ import { CommonModule } from "@angular/common"; -import { Component, Input } from "@angular/core"; -import { Router, RouterLink } from "@angular/router"; +import { Component, Input, OnInit } from "@angular/core"; +import { RouterLink } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -25,31 +25,31 @@ export interface NewItemInitialValues { standalone: true, imports: [NoItemsModule, JslibModule, CommonModule, ButtonModule, RouterLink, MenuModule], }) -export class NewItemDropdownV2Component { +export class NewItemDropdownV2Component implements OnInit { cipherType = CipherType; - + private tab?: chrome.tabs.Tab; /** * Optional initial values to pass to the add cipher form */ @Input() initialValues: NewItemInitialValues; - constructor( - private router: Router, - private dialogService: DialogService, - ) {} + constructor(private dialogService: DialogService) {} - private async buildQueryParams(type: CipherType): Promise { - const tab = await BrowserApi.getTabFromCurrentWindow(); + async ngOnInit() { + this.tab = await BrowserApi.getTabFromCurrentWindow(); + } + + buildQueryParams(type: CipherType): AddEditQueryParams { const poppedOut = BrowserPopupUtils.inPopout(window); const loginDetails: { uri?: string; name?: string } = {}; // When a Login Cipher is created and the extension is not popped out, // pass along the uri and name - if (!poppedOut && type === CipherType.Login && tab) { - loginDetails.uri = tab.url; - loginDetails.name = Utils.getHostname(tab.url); + if (!poppedOut && type === CipherType.Login && this.tab) { + loginDetails.uri = this.tab.url; + loginDetails.name = Utils.getHostname(this.tab.url); } return { @@ -61,10 +61,6 @@ export class NewItemDropdownV2Component { }; } - async newItemNavigate(type: CipherType) { - await this.router.navigate(["/add-cipher"], { queryParams: await this.buildQueryParams(type) }); - } - openFolderDialog() { this.dialogService.open(AddEditFolderDialogComponent); } diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts index 36343d3a661..bc84dd337cb 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts @@ -5,7 +5,12 @@ import { Subject } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { AUTOFILL_ID } from "@bitwarden/common/autofill/constants"; +import { + AUTOFILL_ID, + COPY_PASSWORD_ID, + COPY_USERNAME_ID, + COPY_VERIFICATION_CODE_ID, +} from "@bitwarden/common/autofill/constants"; import { EventType } from "@bitwarden/common/enums"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -17,7 +22,10 @@ import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { CopyCipherFieldService } from "@bitwarden/vault"; +import { BrowserApi } from "../../../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; import { VaultPopupAutofillService } from "./../../../services/vault-popup-autofill.service"; @@ -34,17 +42,26 @@ describe("ViewV2Component", () => { const params$ = new Subject(); const mockNavigate = jest.fn(); const collect = jest.fn().mockResolvedValue(null); - const doAutofill = jest.fn(); + const doAutofill = jest.fn().mockResolvedValue(true); + const copy = jest.fn().mockResolvedValue(true); const mockCipher = { id: "122-333-444", type: CipherType.Login, orgId: "222-444-555", + login: { + username: "test-username", + password: "test-password", + totp: "123", + }, }; const mockVaultPopupAutofillService = { doAutofill, }; + const mockCopyCipherFieldService = { + copy, + }; const mockUserId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); @@ -57,6 +74,7 @@ describe("ViewV2Component", () => { mockNavigate.mockClear(); collect.mockClear(); doAutofill.mockClear(); + copy.mockClear(); await TestBed.configureTestingModule({ imports: [ViewV2Component], @@ -91,6 +109,10 @@ describe("ViewV2Component", () => { canDeleteCipher$: jest.fn().mockReturnValue(true), }, }, + { + provide: CopyCipherFieldService, + useValue: mockCopyCipherFieldService, + }, ], }).compileComponents(); @@ -159,5 +181,46 @@ describe("ViewV2Component", () => { expect(doAutofill).toHaveBeenCalledOnce(); })); + + it('invokes `copy` when action="copy-username"', fakeAsync(() => { + params$.next({ action: COPY_USERNAME_ID }); + + flush(); // Resolve all promises + + expect(copy).toHaveBeenCalledOnce(); + })); + + it('invokes `copy` when action="copy-password"', fakeAsync(() => { + params$.next({ action: COPY_PASSWORD_ID }); + + flush(); // Resolve all promises + + expect(copy).toHaveBeenCalledOnce(); + })); + + it('invokes `copy` when action="copy-totp"', fakeAsync(() => { + params$.next({ action: COPY_VERIFICATION_CODE_ID }); + + flush(); // Resolve all promises + + expect(copy).toHaveBeenCalledOnce(); + })); + + it("closes the popout after a load action", fakeAsync(() => { + jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValueOnce(true); + jest.spyOn(BrowserPopupUtils, "inSingleActionPopout").mockReturnValueOnce(true); + const closeSpy = jest.spyOn(BrowserPopupUtils, "closeSingleActionPopout"); + const focusSpy = jest + .spyOn(BrowserApi, "focusTab") + .mockImplementation(() => Promise.resolve()); + + params$.next({ action: AUTOFILL_ID, senderTabId: 99 }); + + flush(); // Resolve all promises + + expect(doAutofill).toHaveBeenCalledOnce(); + expect(focusSpy).toHaveBeenCalledWith(99); + expect(closeSpy).toHaveBeenCalledOnce(); + })); }); }); 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 8242fd8747e..f739c0ce82e 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 @@ -10,7 +10,13 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { AUTOFILL_ID, SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants"; +import { + AUTOFILL_ID, + COPY_PASSWORD_ID, + COPY_USERNAME_ID, + COPY_VERIFICATION_CODE_ID, + SHOW_AUTOFILL_BUTTON, +} from "@bitwarden/common/autofill/constants"; import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -18,7 +24,6 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { AsyncActionsModule, @@ -28,19 +33,34 @@ import { SearchModule, ToastService, } from "@bitwarden/components"; +import { CopyCipherFieldService } from "@bitwarden/vault"; import { PremiumUpgradePromptService } from "../../../../../../../../libs/common/src/vault/abstractions/premium-upgrade-prompt.service"; import { CipherViewComponent } from "../../../../../../../../libs/vault/src/cipher-view"; +import { BrowserApi } from "../../../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component"; import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service"; import { BrowserViewPasswordHistoryService } from "../../../services/browser-view-password-history.service"; +import { closeViewVaultItemPopout, VaultPopoutType } from "../../../utils/vault-popout-window"; 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"; import { VaultPopupAutofillService } from "./../../../services/vault-popup-autofill.service"; +/** + * The types of actions that can be triggered when loading the view vault item popout via the + * extension ContextMenu. See context-menu-clicked-handler.ts for more information. + */ +type LoadAction = + | typeof AUTOFILL_ID + | typeof SHOW_AUTOFILL_BUTTON + | typeof COPY_USERNAME_ID + | typeof COPY_PASSWORD_ID + | typeof COPY_VERIFICATION_CODE_ID; + @Component({ selector: "app-view-v2", templateUrl: "view-v2.component.html", @@ -68,10 +88,10 @@ export class ViewV2Component { headerText: string; cipher: CipherView; organization$: Observable; - folder$: Observable; canDeleteCipher$: Observable; collections$: Observable; - loadAction: typeof AUTOFILL_ID | typeof SHOW_AUTOFILL_BUTTON; + loadAction: LoadAction; + senderTabId?: number; constructor( private route: ActivatedRoute, @@ -86,6 +106,7 @@ export class ViewV2Component { private eventCollectionService: EventCollectionService, private popupRouterCacheService: PopupRouterCacheService, protected cipherAuthorizationService: CipherAuthorizationService, + private copyCipherFieldService: CopyCipherFieldService, ) { this.subscribeToParams(); } @@ -95,13 +116,15 @@ export class ViewV2Component { .pipe( switchMap(async (params): Promise => { this.loadAction = params.action; + this.senderTabId = params.senderTabId ? parseInt(params.senderTabId, 10) : undefined; return await this.getCipherData(params.cipherId); }), switchMap(async (cipher) => { this.cipher = cipher; this.headerText = this.setHeader(cipher.type); - if (this.loadAction === AUTOFILL_ID) { - await this.vaultPopupAutofillService.doAutofill(this.cipher); + + if (this.loadAction) { + await this._handleLoadAction(this.loadAction, this.senderTabId); } this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(cipher); @@ -211,4 +234,65 @@ export class ViewV2Component { protected showFooter(): boolean { return this.cipher && (!this.cipher.isDeleted || (this.cipher.isDeleted && this.cipher.edit)); } + + /** + * Handles the load action for the view vault item popout. These actions are typically triggered + * via the extension context menu. It is necessary to render the view for items that have password + * reprompt enabled. + * @param loadAction + * @param senderTabId + * @private + */ + private async _handleLoadAction(loadAction: LoadAction, senderTabId?: number): Promise { + let actionSuccess = false; + + // Both vaultPopupAutofillService and copyCipherFieldService will perform password re-prompting internally. + + switch (loadAction) { + case "show-autofill-button": + // This action simply shows the cipher view, no need to do anything. + return; + case "autofill": + actionSuccess = await this.vaultPopupAutofillService.doAutofill(this.cipher, false); + break; + case "copy-username": + actionSuccess = await this.copyCipherFieldService.copy( + this.cipher.login.username, + "username", + this.cipher, + ); + break; + case "copy-password": + actionSuccess = await this.copyCipherFieldService.copy( + this.cipher.login.password, + "password", + this.cipher, + ); + break; + case "copy-totp": + actionSuccess = await this.copyCipherFieldService.copy( + this.cipher.login.totp, + "totp", + this.cipher, + ); + break; + } + + if (BrowserPopupUtils.inPopout(window)) { + setTimeout( + async () => { + if ( + BrowserPopupUtils.inSingleActionPopout(window, VaultPopoutType.viewVaultItem) && + senderTabId + ) { + await BrowserApi.focusTab(senderTabId); + await closeViewVaultItemPopout(`${VaultPopoutType.viewVaultItem}_${this.cipher.id}`); + } else { + await this.popupRouterCacheService.back(); + } + }, + actionSuccess ? 1000 : 0, + ); + } + } } 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 f5c28b2bebd..bf557f74608 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 @@ -118,6 +118,7 @@ type="button" class="box-content-row" appStopClick + *ngIf="isSshKeysEnabled" (click)="selectType(cipherType.SshKey)" >
diff --git a/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts b/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts index 448f85a8cbd..d12b2fd801d 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts @@ -7,7 +7,9 @@ import { first, switchMap, takeUntil } from "rxjs/operators"; import { CollectionView } from "@bitwarden/admin-console/common"; import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/platform/sync"; @@ -62,6 +64,8 @@ export class VaultFilterComponent implements OnInit, OnDestroy { selectedOrganization: string = null; showCollections = true; + isSshKeysEnabled = false; + private loadedTimeout: number; private selectedTimeout: number; private preventSelected = false; @@ -95,6 +99,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { private location: Location, private vaultFilterService: VaultFilterService, private vaultBrowserStateService: VaultBrowserStateService, + private configService: ConfigService, ) { this.noFolderListSize = 100; } @@ -166,6 +171,8 @@ export class VaultFilterComponent implements OnInit, OnDestroy { .subscribe((isSearchable) => { this.isSearchable = isSearchable; }); + + this.isSshKeysEnabled = await this.configService.getFeatureFlag(FeatureFlag.SSHKeyVaultItem); } ngOnDestroy() { diff --git a/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts b/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts index 151f8517d57..2b37b26b9cb 100644 --- a/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts +++ b/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts @@ -24,8 +24,7 @@ export const GLOBAL_VAULT_UI_ONBOARDING = new KeyDefinition( @Injectable() export class VaultUiOnboardingService { - // TODO: Update this date to the release date of the new Browser UI - private onboardingUiReleaseDate = new Date("2024-07-25"); + private onboardingUiReleaseDate = new Date("2024-12-10"); private vaultUiOnboardingState: GlobalState = this.stateProvider.getGlobal( GLOBAL_VAULT_UI_ONBOARDING, diff --git a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.html b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.html index 69322b08c8a..a79b6c74b03 100644 --- a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.html +++ b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.html @@ -30,7 +30,7 @@ - diff --git a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts index de9d95aab00..6827823704e 100644 --- a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component, Input } from "@angular/core"; +import { ChangeDetectionStrategy, Component, Input } from "@angular/core"; import { Router } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -17,7 +17,7 @@ import { ToastService, TypographyModule, } from "@bitwarden/components"; -import { PasswordRepromptService } from "@bitwarden/vault"; +import { CanDeleteCipherDirective, PasswordRepromptService } from "@bitwarden/vault"; @Component({ selector: "app-trash-list-items-container", @@ -29,10 +29,12 @@ import { PasswordRepromptService } from "@bitwarden/vault"; JslibModule, SectionComponent, SectionHeaderComponent, + CanDeleteCipherDirective, MenuModule, IconButtonModule, TypographyModule, ], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class TrashListItemsContainerComponent { /** diff --git a/apps/browser/src/vault/popup/settings/trash.component.ts b/apps/browser/src/vault/popup/settings/trash.component.ts index b6f77ef6a52..8bac22df53f 100644 --- a/apps/browser/src/vault/popup/settings/trash.component.ts +++ b/apps/browser/src/vault/popup/settings/trash.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; +import { ChangeDetectionStrategy, Component } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { CalloutModule, NoItemsModule } from "@bitwarden/components"; @@ -27,6 +27,7 @@ import { TrashListItemsContainerComponent } from "./trash-list-items-container/t CalloutModule, NoItemsModule, ], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class TrashComponent { protected deletedCiphers$ = this.vaultPopupItemsService.deletedCiphers$; diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index fc014534e03..3f8702d000d 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -21,7 +21,6 @@ import { LoginExport } from "@bitwarden/common/models/export/login.export"; import { SecureNoteExport } from "@bitwarden/common/models/export/secure-note.export"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; @@ -58,7 +57,6 @@ export class GetCommand extends DownloadCommand { private auditService: AuditService, private keyService: KeyService, encryptService: EncryptService, - private stateService: StateService, private searchService: SearchService, private apiService: ApiService, private organizationService: OrganizationService, diff --git a/apps/cli/src/oss-serve-configurator.ts b/apps/cli/src/oss-serve-configurator.ts index a25357f6f69..0614aa05e13 100644 --- a/apps/cli/src/oss-serve-configurator.ts +++ b/apps/cli/src/oss-serve-configurator.ts @@ -58,7 +58,6 @@ export class OssServeConfigurator { this.serviceContainer.auditService, this.serviceContainer.keyService, this.serviceContainer.encryptService, - this.serviceContainer.stateService, this.serviceContainer.searchService, this.serviceContainer.apiService, this.serviceContainer.organizationService, diff --git a/apps/cli/src/tools/generate.command.ts b/apps/cli/src/tools/generate.command.ts index 2829bc51414..0a17e4843b9 100644 --- a/apps/cli/src/tools/generate.command.ts +++ b/apps/cli/src/tools/generate.command.ts @@ -33,7 +33,7 @@ export class GenerateCommand { includeNumber: normalizedOptions.includeNumber, minNumber: normalizedOptions.minNumber, minSpecial: normalizedOptions.minSpecial, - ambiguous: normalizedOptions.ambiguous, + ambiguous: !normalizedOptions.ambiguous, }; const enforcedOptions = (await this.stateService.getIsAuthenticated()) diff --git a/apps/cli/src/tools/send/send.program.ts b/apps/cli/src/tools/send/send.program.ts index 60e78137e7e..d70b8102213 100644 --- a/apps/cli/src/tools/send/send.program.ts +++ b/apps/cli/src/tools/send/send.program.ts @@ -144,7 +144,6 @@ export class SendProgram extends BaseProgram { this.serviceContainer.auditService, this.serviceContainer.keyService, this.serviceContainer.encryptService, - this.serviceContainer.stateService, this.serviceContainer.searchService, this.serviceContainer.apiService, this.serviceContainer.organizationService, diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index 4d3215944e7..252818c2f26 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -179,7 +179,6 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.auditService, this.serviceContainer.keyService, this.serviceContainer.encryptService, - this.serviceContainer.stateService, this.serviceContainer.searchService, this.serviceContainer.apiService, this.serviceContainer.organizationService, diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 9fe34f891c1..9e9be3dbaef 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -36,6 +36,7 @@ dependencies = [ "cfg-if", "cipher", "cpufeatures", + "zeroize", ] [[package]] @@ -61,6 +62,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + [[package]] name = "anyhow" version = "1.0.93" @@ -83,6 +90,19 @@ dependencies = [ "x11rb", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", + "zeroize", +] + [[package]] name = "async-broadcast" version = "0.7.1" @@ -133,9 +153,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.4" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" dependencies = [ "async-lock", "cfg-if", @@ -161,6 +181,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + [[package]] name = "async-process" version = "2.3.0" @@ -320,6 +351,15 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -378,9 +418,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cbc" @@ -393,23 +433,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.34" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9" +checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc" dependencies = [ "shlex", ] -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -447,8 +477,35 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] +[[package]] +name = "clap" +version = "4.5.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69371e34337c4c984bbe322360c2547210bf632eb2814bbe78a6e87a2935bd2b" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e24c1b4099818523236a8ca881d2b45db98dadfb4625cf6608c12069fcbbde1" +dependencies = [ + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" + [[package]] name = "clipboard-win" version = "5.4.0" @@ -492,6 +549,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.0" @@ -510,9 +577,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -530,14 +597,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] [[package]] name = "ctor" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", "syn", @@ -580,25 +648,26 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.129" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbdc8cca144dce1c4981b5c9ab748761619979e515c3d53b5df385c677d1d007" +checksum = "05e1ec88093d2abd9cf1b09ffd979136b8e922bf31cad966a8fe0d73233112ef" dependencies = [ "cc", + "cxxbridge-cmd", "cxxbridge-flags", "cxxbridge-macro", + "foldhash", "link-cplusplus", ] [[package]] name = "cxx-build" -version = "1.0.129" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5764c3142ab44fcf857101d12c0ddf09c34499900557c764f5ad0597159d1fc" +checksum = "9afa390d956ee7ccb41aeed7ed7856ab3ffb4fc587e7216be7e0f83e949b4e6c" dependencies = [ "cc", "codespan-reporting", - "once_cell", "proc-macro2", "quote", "scratch", @@ -606,19 +675,33 @@ dependencies = [ ] [[package]] -name = "cxxbridge-flags" -version = "1.0.129" +name = "cxxbridge-cmd" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d422aff542b4fa28c2ce8e5cc202d42dbf24702345c1fba3087b2d3f8a1b90ff" +checksum = "3c23bfff654d6227cbc83de8e059d2f8678ede5fc3a6c5a35d5c379983cc61e6" +dependencies = [ + "clap", + "codespan-reporting", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c01b36e22051bc6928a78583f1621abaaf7621561c2ada1b00f7878fbe2caa" [[package]] name = "cxxbridge-macro" -version = "1.0.129" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1719100f31492cd6adeeab9a0f46cdbc846e615fdb66d7b398aa46ec7fdd06f" +checksum = "f6e14013136fac689345d17b9a6df55977251f11d333c0a571e8d963b55e1f95" dependencies = [ "proc-macro2", "quote", + "rustversion", "syn", ] @@ -660,22 +743,23 @@ dependencies = [ "aes", "anyhow", "arboard", + "argon2", "async-stream", "base64", "bitwarden-russh", "byteorder", "cbc", - "core-foundation", + "core-foundation 0.10.0", + "desktop_objc", "dirs", "ed25519", "futures", - "gio", "homedir", "interprocess", "keytar", "libc", - "libsecret", "log", + "oo7", "pin-project", "pkcs8", "rand", @@ -718,6 +802,18 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "desktop_objc" +version = "0.0.0" +dependencies = [ + "anyhow", + "cc", + "core-foundation 0.9.4", + "glob", + "thiserror", + "tokio", +] + [[package]] name = "desktop_proxy" version = "0.0.0" @@ -849,12 +945,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -876,9 +972,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" dependencies = [ "event-listener", "pin-project-lite", @@ -886,9 +982,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" [[package]] name = "fiat-crypto" @@ -908,6 +1004,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "futures" version = "0.3.31" @@ -958,9 +1060,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1fa2f9765705486b33fd2acf1577f8ec449c2ba1f318ae5447697b7c08d210" +checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" dependencies = [ "fastrand", "futures-core", @@ -1058,109 +1160,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] -name = "gio" -version = "0.19.5" +name = "glob" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be548be810e45dd31d3bbb89c6210980bb7af9bca3ea1292b5f16b75f8e394a7" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "gio-sys", - "glib", - "libc", - "pin-project-lite", - "smallvec", - "thiserror", -] - -[[package]] -name = "gio-sys" -version = "0.19.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd743ba4714d671ad6b6234e8ab2a13b42304d0e13ab7eba1dcdd78a7d6d4ef" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "windows-sys 0.52.0", -] - -[[package]] -name = "glib" -version = "0.19.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39650279f135469465018daae0ba53357942a5212137515777d5fdca74984a44" -dependencies = [ - "bitflags", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", - "libc", - "memchr", - "smallvec", - "thiserror", -] - -[[package]] -name = "glib-macros" -version = "0.19.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4429b0277a14ae9751350ad9b658b1be0abb5b54faa5bcdf6e74a3372582fad7" -dependencies = [ - "heck", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "glib-sys" -version = "0.19.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c2dc18d3a82b0006d470b13304fbbb3e0a9bd4884cf985a60a7ed733ac2c4a5" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "gobject-sys" -version = "0.19.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e697e252d6e0416fd1d9e169bda51c0f1c926026c39ca21fbe8b1bb5c3b8b9e" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "hashbrown" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "hermit-abi" @@ -1174,6 +1183,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -1197,9 +1215,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown", @@ -1232,9 +1250,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "keytar" @@ -1274,9 +1292,9 @@ checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" [[package]] name = "libloading" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", "windows-targets 0.52.6", @@ -1284,9 +1302,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libredox" @@ -1298,32 +1316,6 @@ dependencies = [ "libc", ] -[[package]] -name = "libsecret" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c6ccddc706a38eca477b4d7857acd6c76c7d6fba5d47b4b2e7d800e5a17194" -dependencies = [ - "gio", - "glib", - "libc", - "libsecret-sys", -] - -[[package]] -name = "libsecret-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1af48e61f1c8e77e9705296f346e45b637754a92348a79b4c62df84d0654c2" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pkg-config", - "system-deps", -] - [[package]] name = "link-cplusplus" version = "1.0.9" @@ -1355,6 +1347,16 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1387,11 +1389,10 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi 0.3.9", "libc", "wasi", "windows-sys 0.52.0", @@ -1419,9 +1420,9 @@ checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a" [[package]] name = "napi-derive" -version = "2.16.12" +version = "2.16.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17435f7a00bfdab20b0c27d9c56f58f6499e418252253081bfff448099da31d1" +checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c" dependencies = [ "cfg-if", "convert_case", @@ -1433,9 +1434,9 @@ dependencies = [ [[package]] name = "napi-derive-backend" -version = "1.0.74" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "967c485e00f0bf3b1bdbe510a38a4606919cf1d34d9a37ad41f25a81aa077abe" +checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf" dependencies = [ "convert_case", "once_cell", @@ -1490,6 +1491,30 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1503,10 +1528,20 @@ dependencies = [ "num-iter", "num-traits", "rand", + "serde", "smallvec", "zeroize", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1533,6 +1568,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1666,6 +1712,39 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "oo7" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc6ce4692fbfd044ce22ca07dcab1a30fa12432ca2aa5b1294eca50d3332a24" +dependencies = [ + "aes", + "async-fs", + "async-io", + "async-lock", + "async-net", + "blocking", + "cbc", + "cipher", + "digest", + "endi", + "futures-lite", + "futures-util", + "hkdf", + "hmac", + "md-5", + "num", + "num-bigint-dig", + "pbkdf2", + "rand", + "serde", + "sha2", + "subtle", + "zbus", + "zeroize", + "zvariant", +] + [[package]] name = "opaque-debug" version = "0.3.1" @@ -1727,6 +1806,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "pbkdf2" version = "0.12.2" @@ -1845,13 +1935,13 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "polling" -version = "3.7.3" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.4.0", + "hermit-abi", "pin-project-lite", "rustix", "tracing", @@ -1907,9 +1997,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -2002,9 +2092,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -2065,18 +2155,18 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" dependencies = [ "bitflags", "errno", @@ -2085,6 +2175,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + [[package]] name = "salsa20" version = "0.10.2" @@ -2130,7 +2226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d0283c0a4a22a0f1b0e4edca251aa20b92fc96eaa09b84bec052f9415e9d71" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.10.0", "core-foundation-sys", "libc", "security-framework-sys", @@ -2154,18 +2250,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", @@ -2183,15 +2279,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_spanned" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" -dependencies = [ - "serde", -] - [[package]] name = "sha1" version = "0.10.6" @@ -2267,9 +2354,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -2344,6 +2431,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -2352,39 +2445,20 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.87" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - [[package]] name = "tempfile" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", @@ -2424,9 +2498,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -2447,9 +2521,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", @@ -2506,26 +2580,11 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.8.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" -dependencies = [ - "serde", -] [[package]] name = "toml_edit" @@ -2534,17 +2593,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", - "serde", - "serde_spanned", "toml_datetime", "winnow", ] [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -2553,9 +2610,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", @@ -2564,9 +2621,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] @@ -2603,9 +2660,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-segmentation" @@ -2629,12 +2686,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "version-compare" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" - [[package]] name = "version_check" version = "0.9.5" @@ -3199,6 +3250,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zeroizing-alloc" diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index 58bdafebcce..49c4558af65 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -10,23 +10,22 @@ default = ["sys"] manual_test = [] sys = [ - "dep:widestring", - "dep:windows", - "dep:core-foundation", - "dep:security-framework", - "dep:security-framework-sys", - "dep:gio", - "dep:libsecret", - "dep:zbus", - "dep:zbus_polkit", + "dep:widestring", + "dep:windows", + "dep:core-foundation", + "dep:security-framework", + "dep:security-framework-sys", + "dep:zbus", + "dep:zbus_polkit", ] [dependencies] aes = "=0.8.4" anyhow = "=1.0.93" arboard = { version = "=3.4.1", default-features = false, features = [ - "wayland-data-control", + "wayland-data-control", ] } +argon2 = { version = "=0.5.3", features = ["zeroize"] } async-stream = "=0.3.6" base64 = "=0.22.1" byteorder = "=1.5.0" @@ -45,10 +44,10 @@ scopeguard = "=1.2.0" sha2 = "=0.10.8" ssh-encoding = "=0.2.0" ssh-key = { version = "=0.6.7", default-features = false, features = [ - "encryption", - "ed25519", - "rsa", - "getrandom", + "encryption", + "ed25519", + "rsa", + "getrandom", ] } bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "b4e7f2fedbe3df8c35545feb000176d3e7b2bc32" } tokio = { version = "=1.41.1", features = ["io-util", "sync", "macros", "net"] } @@ -83,9 +82,10 @@ keytar = "=0.1.6" 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 } +desktop_objc = { path = "../objc" } [target.'cfg(target_os = "linux")'.dependencies] -gio = { version = "=0.19.5", optional = true } -libsecret = { version = "=0.5.0", optional = true } +oo7 = "=0.3.3" + zbus = { version = "=4.4.0", optional = true } zbus_polkit = { version = "=4.0.0", optional = true } diff --git a/apps/desktop/desktop_native/core/src/autofill/macos.rs b/apps/desktop/desktop_native/core/src/autofill/macos.rs new file mode 100644 index 00000000000..08f333abe93 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/autofill/macos.rs @@ -0,0 +1,5 @@ +use anyhow::Result; + +pub async fn run_command(value: String) -> Result { + desktop_objc::run_command(value).await +} diff --git a/apps/desktop/desktop_native/core/src/autofill/mod.rs b/apps/desktop/desktop_native/core/src/autofill/mod.rs new file mode 100644 index 00000000000..5997add240f --- /dev/null +++ b/apps/desktop/desktop_native/core/src/autofill/mod.rs @@ -0,0 +1,5 @@ +#[cfg_attr(target_os = "linux", path = "unix.rs")] +#[cfg_attr(target_os = "windows", path = "windows.rs")] +#[cfg_attr(target_os = "macos", path = "macos.rs")] +mod autofill; +pub use autofill::*; diff --git a/apps/desktop/desktop_native/core/src/autofill/unix.rs b/apps/desktop/desktop_native/core/src/autofill/unix.rs new file mode 100644 index 00000000000..d77130176a4 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/autofill/unix.rs @@ -0,0 +1,5 @@ +use anyhow::Result; + +pub async fn run_command(value: String) -> Result { + todo!("Unix does not support autofill"); +} diff --git a/apps/desktop/desktop_native/core/src/autofill/windows.rs b/apps/desktop/desktop_native/core/src/autofill/windows.rs new file mode 100644 index 00000000000..2e442263c1b --- /dev/null +++ b/apps/desktop/desktop_native/core/src/autofill/windows.rs @@ -0,0 +1,5 @@ +use anyhow::Result; + +pub async fn run_command(value: String) -> Result { + todo!("Windows does not support autofill"); +} diff --git a/apps/desktop/desktop_native/core/src/biometric/macos.rs b/apps/desktop/desktop_native/core/src/biometric/macos.rs index 01ee4519ce6..ec09d566e1f 100644 --- a/apps/desktop/desktop_native/core/src/biometric/macos.rs +++ b/apps/desktop/desktop_native/core/src/biometric/macos.rs @@ -18,7 +18,7 @@ impl super::BiometricTrait for Biometric { bail!("platform not supported"); } - fn get_biometric_secret( + async fn get_biometric_secret( _service: &str, _account: &str, _key_material: Option, @@ -26,7 +26,7 @@ impl super::BiometricTrait for Biometric { bail!("platform not supported"); } - fn set_biometric_secret( + async fn set_biometric_secret( _service: &str, _account: &str, _secret: &str, diff --git a/apps/desktop/desktop_native/core/src/biometric/mod.rs b/apps/desktop/desktop_native/core/src/biometric/mod.rs index 72352cf2288..dd480f817b6 100644 --- a/apps/desktop/desktop_native/core/src/biometric/mod.rs +++ b/apps/desktop/desktop_native/core/src/biometric/mod.rs @@ -22,20 +22,19 @@ pub struct OsDerivedKey { pub iv_b64: String, } +#[allow(async_fn_in_trait)] pub trait BiometricTrait { - #[allow(async_fn_in_trait)] async fn prompt(hwnd: Vec, message: String) -> Result; - #[allow(async_fn_in_trait)] async fn available() -> Result; fn derive_key_material(secret: Option<&str>) -> Result; - fn set_biometric_secret( + async fn set_biometric_secret( service: &str, account: &str, secret: &str, key_material: Option, iv_b64: &str, ) -> Result; - fn get_biometric_secret( + async fn get_biometric_secret( service: &str, account: &str, key_material: Option, diff --git a/apps/desktop/desktop_native/core/src/biometric/unix.rs b/apps/desktop/desktop_native/core/src/biometric/unix.rs index 563bd1dfe52..5153fc5ed87 100644 --- a/apps/desktop/desktop_native/core/src/biometric/unix.rs +++ b/apps/desktop/desktop_native/core/src/biometric/unix.rs @@ -73,7 +73,7 @@ impl super::BiometricTrait for Biometric { Ok(OsDerivedKey { key_b64, iv_b64 }) } - fn set_biometric_secret( + async fn set_biometric_secret( service: &str, account: &str, secret: &str, @@ -85,11 +85,11 @@ impl super::BiometricTrait for Biometric { ))?; let encrypted_secret = encrypt(secret, &key_material, iv_b64)?; - crate::password::set_password(service, account, &encrypted_secret)?; + crate::password::set_password(service, account, &encrypted_secret).await?; Ok(encrypted_secret) } - fn get_biometric_secret( + async fn get_biometric_secret( service: &str, account: &str, key_material: Option, @@ -98,7 +98,7 @@ impl super::BiometricTrait for Biometric { "Key material is required for polkit protected keys" ))?; - let encrypted_secret = crate::password::get_password(service, account)?; + let encrypted_secret = crate::password::get_password(service, account).await?; let secret = CipherString::from_str(&encrypted_secret)?; return Ok(decrypt(&secret, &key_material)?); } diff --git a/apps/desktop/desktop_native/core/src/biometric/windows.rs b/apps/desktop/desktop_native/core/src/biometric/windows.rs index c951e42e260..fcc5b95cc4a 100644 --- a/apps/desktop/desktop_native/core/src/biometric/windows.rs +++ b/apps/desktop/desktop_native/core/src/biometric/windows.rs @@ -121,7 +121,7 @@ impl super::BiometricTrait for Biometric { Ok(OsDerivedKey { key_b64, iv_b64 }) } - fn set_biometric_secret( + async fn set_biometric_secret( service: &str, account: &str, secret: &str, @@ -133,11 +133,11 @@ impl super::BiometricTrait for Biometric { ))?; let encrypted_secret = encrypt(secret, &key_material, iv_b64)?; - crate::password::set_password(service, account, &encrypted_secret)?; + crate::password::set_password(service, account, &encrypted_secret).await?; Ok(encrypted_secret) } - fn get_biometric_secret( + async fn get_biometric_secret( service: &str, account: &str, key_material: Option, @@ -146,7 +146,7 @@ impl super::BiometricTrait for Biometric { "Key material is required for Windows Hello protected keys" ))?; - let encrypted_secret = crate::password::get_password(service, account)?; + let encrypted_secret = crate::password::get_password(service, account).await?; match CipherString::from_str(&encrypted_secret) { Ok(secret) => { // If the secret is a CipherString, it is encrypted and we need to decrypt it. @@ -292,9 +292,9 @@ mod tests { assert_eq!(decrypt(&secret, &key_material).unwrap(), "secret") } - #[test] - fn get_biometric_secret_requires_key() { - let result = ::get_biometric_secret("", "", None); + #[tokio::test] + async fn get_biometric_secret_requires_key() { + let result = ::get_biometric_secret("", "", None).await; assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), @@ -302,29 +302,25 @@ mod tests { ); } - #[test] - fn get_biometric_secret_handles_unencrypted_secret() { - scopeguard::defer! { - crate::password::delete_password("test", "test").unwrap(); - } + #[tokio::test] + async fn get_biometric_secret_handles_unencrypted_secret() { let test = "test"; let secret = "password"; let key_material = KeyMaterial { os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(), client_key_part_b64: Some("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned()), }; - crate::password::set_password(test, test, secret).unwrap(); + crate::password::set_password(test, test, secret).await.unwrap(); let result = ::get_biometric_secret(test, test, Some(key_material)) + .await .unwrap(); + crate::password::delete_password("test", "test").await.unwrap(); assert_eq!(result, secret); } - #[test] - fn get_biometric_secret_handles_encrypted_secret() { - scopeguard::defer! { - crate::password::delete_password("test", "test").unwrap(); - } + #[tokio::test] + async fn get_biometric_secret_handles_encrypted_secret() { let test = "test"; let secret = CipherString::from_str("0.l9fhDUP/wDJcKwmEzcb/3w==|uP4LcqoCCj5FxBDP77NV6Q==").unwrap(); // output from test_encrypt @@ -332,17 +328,19 @@ mod tests { os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(), client_key_part_b64: Some("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned()), }; - crate::password::set_password(test, test, &secret.to_string()).unwrap(); + crate::password::set_password(test, test, &secret.to_string()).await.unwrap(); let result = ::get_biometric_secret(test, test, Some(key_material)) + .await .unwrap(); + crate::password::delete_password("test", "test").await.unwrap(); assert_eq!(result, "secret"); } - #[test] - fn set_biometric_secret_requires_key() { - let result = ::set_biometric_secret("", "", "", None, ""); + #[tokio::test] + async fn set_biometric_secret_requires_key() { + let result = ::set_biometric_secret("", "", "", None, "").await; assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), diff --git a/apps/desktop/desktop_native/core/src/crypto/crypto.rs b/apps/desktop/desktop_native/core/src/crypto/crypto.rs index b1fd4426fa6..4427138cb1d 100644 --- a/apps/desktop/desktop_native/core/src/crypto/crypto.rs +++ b/apps/desktop/desktop_native/core/src/crypto/crypto.rs @@ -5,7 +5,7 @@ use aes::cipher::{ BlockEncryptMut, KeyIvInit, }; -use crate::error::{CryptoError, Result}; +use crate::error::{CryptoError, KdfParamError, Result}; use super::CipherString; @@ -37,3 +37,53 @@ pub fn encrypt_aes256( Ok(CipherString::AesCbc256_B64 { iv, data }) } + +pub fn argon2( + secret: &[u8], + salt: &[u8], + iterations: u32, + memory: u32, + parallelism: u32, +) -> Result<[u8; 32]> { + use argon2::*; + + let params = Params::new(memory, iterations, parallelism, Some(32)).map_err(|e| { + KdfParamError::InvalidParams(format!("Argon2 parameters are invalid: {e}",)) + })?; + let argon = Argon2::new(Algorithm::Argon2id, Version::V0x13, params); + + let mut hash = [0u8; 32]; + argon + .hash_password_into(secret, &salt, &mut hash) + .map_err(|e| KdfParamError::InvalidParams(format!("Argon2 hashing failed: {e}",)))?; + + // Argon2 is using some stack memory that is not zeroed. Eventually some function will + // overwrite the stack, but we use this trick to force the used stack to be zeroed. + #[inline(never)] + fn clear_stack() { + std::hint::black_box([0u8; 4096]); + } + clear_stack(); + Ok(hash) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_argon2() { + let test_hash: [u8; 32] = [ + 112, 200, 85, 209, 100, 4, 246, 146, 117, 180, 152, 44, 103, 198, 75, 14, 166, 77, 201, + 22, 62, 178, 87, 224, 95, 209, 253, 68, 166, 209, 47, 218, + ]; + let secret = b"supersecurepassword"; + let salt = b"mail@example.com"; + let iterations = 3; + let memory = 1024 * 64; + let parallelism = 4; + + let hash = argon2(secret, salt, iterations, memory, parallelism).unwrap(); + assert_eq!(hash, test_hash,); + } +} diff --git a/apps/desktop/desktop_native/core/src/error.rs b/apps/desktop/desktop_native/core/src/error.rs index d3104cb6f44..34624ed630e 100644 --- a/apps/desktop/desktop_native/core/src/error.rs +++ b/apps/desktop/desktop_native/core/src/error.rs @@ -9,6 +9,8 @@ pub enum Error { #[error("Cryptography Error, {0}")] Crypto(#[from] CryptoError), + #[error("KDF Parameter Error, {0}")] + KdfParam(#[from] KdfParamError), } #[derive(Debug, Error)] @@ -29,6 +31,12 @@ pub enum CryptoError { KeyDecrypt, } +#[derive(Debug, Error)] +pub enum KdfParamError { + #[error("Invalid KDF parameters: {0}")] + InvalidParams(String), +} + // Ensure that the error messages implement Send and Sync #[cfg(test)] const _: () = { diff --git a/apps/desktop/desktop_native/core/src/lib.rs b/apps/desktop/desktop_native/core/src/lib.rs index fe3f0f96a18..811b403a4cb 100644 --- a/apps/desktop/desktop_native/core/src/lib.rs +++ b/apps/desktop/desktop_native/core/src/lib.rs @@ -1,3 +1,4 @@ +pub mod autofill; #[cfg(feature = "sys")] pub mod biometric; #[cfg(feature = "sys")] @@ -8,10 +9,10 @@ pub mod ipc; #[cfg(feature = "sys")] pub mod password; #[cfg(feature = "sys")] -pub mod process_isolation; -#[cfg(feature = "sys")] pub mod powermonitor; #[cfg(feature = "sys")] +pub mod process_isolation; +#[cfg(feature = "sys")] pub mod ssh_agent; #[cfg(feature = "sys")] pub mod epheremal_values; diff --git a/apps/desktop/desktop_native/core/src/password/macos.rs b/apps/desktop/desktop_native/core/src/password/macos.rs index 408706423e2..c911a0d2430 100644 --- a/apps/desktop/desktop_native/core/src/password/macos.rs +++ b/apps/desktop/desktop_native/core/src/password/macos.rs @@ -3,26 +3,22 @@ use security_framework::passwords::{ delete_generic_password, get_generic_password, set_generic_password, }; -pub fn get_password(service: &str, account: &str) -> Result { +pub async fn get_password(service: &str, account: &str) -> Result { let result = String::from_utf8(get_generic_password(&service, &account)?)?; Ok(result) } -pub fn get_password_keytar(service: &str, account: &str) -> Result { - get_password(service, account) -} - -pub fn set_password(service: &str, account: &str, password: &str) -> Result<()> { +pub async fn set_password(service: &str, account: &str, password: &str) -> Result<()> { let result = set_generic_password(&service, &account, password.as_bytes())?; Ok(result) } -pub fn delete_password(service: &str, account: &str) -> Result<()> { +pub async fn delete_password(service: &str, account: &str) -> Result<()> { let result = delete_generic_password(&service, &account)?; Ok(result) } -pub fn is_available() -> Result { +pub async fn is_available() -> Result { Ok(true) } @@ -30,18 +26,17 @@ pub fn is_available() -> Result { mod tests { use super::*; - #[test] - fn test() { - scopeguard::defer!(delete_password("BitwardenTest", "BitwardenTest").unwrap_or({});); - set_password("BitwardenTest", "BitwardenTest", "Random").unwrap(); + #[tokio::test] + async fn test() { + set_password("BitwardenTest", "BitwardenTest", "Random").await.unwrap(); assert_eq!( "Random", - get_password("BitwardenTest", "BitwardenTest").unwrap() + get_password("BitwardenTest", "BitwardenTest").await.unwrap() ); - delete_password("BitwardenTest", "BitwardenTest").unwrap(); + delete_password("BitwardenTest", "BitwardenTest").await.unwrap(); // Ensure password is deleted - match get_password("BitwardenTest", "BitwardenTest") { + match get_password("BitwardenTest", "BitwardenTest").await { Ok(_) => panic!("Got a result"), Err(e) => assert_eq!( "The specified item could not be found in the keychain.", @@ -50,9 +45,9 @@ mod tests { } } - #[test] - fn test_error_no_password() { - match get_password("Unknown", "Unknown") { + #[tokio::test] + async fn test_error_no_password() { + match get_password("Unknown", "Unknown").await { Ok(_) => panic!("Got a result"), Err(e) => assert_eq!( "The specified item could not be found in the keychain.", diff --git a/apps/desktop/desktop_native/core/src/password/unix.rs b/apps/desktop/desktop_native/core/src/password/unix.rs index 1817a4d62ee..20a79625efb 100644 --- a/apps/desktop/desktop_native/core/src/password/unix.rs +++ b/apps/desktop/desktop_native/core/src/password/unix.rs @@ -1,106 +1,106 @@ use anyhow::{anyhow, Result}; -use libsecret::{password_clear_sync, password_lookup_sync, password_store_sync, Schema}; +use oo7::dbus::{self}; use std::collections::HashMap; -pub fn get_password(service: &str, account: &str) -> Result { - let res = password_lookup_sync( - Some(&get_schema()), - build_attributes(service, account), - gio::Cancellable::NONE, - )?; - - match res { - Some(s) => Ok(String::from(s)), - None => Err(anyhow!("No password found")), - } -} - -pub fn get_password_keytar(service: &str, account: &str) -> Result { - get_password(service, account) -} - -pub fn set_password(service: &str, account: &str, password: &str) -> Result<()> { - let result = password_store_sync( - Some(&get_schema()), - build_attributes(service, account), - Some(&libsecret::COLLECTION_DEFAULT), - &format!("{}/{}", service, account), - password, - gio::Cancellable::NONE, - )?; - Ok(result) -} - -pub fn delete_password(service: &str, account: &str) -> Result<()> { - let result = password_clear_sync( - Some(&get_schema()), - build_attributes(service, account), - gio::Cancellable::NONE, - )?; - Ok(result) -} - -pub fn is_available() -> Result { - let result = password_clear_sync( - Some(&get_schema()), - build_attributes("bitwardenSecretsAvailabilityTest", "test"), - gio::Cancellable::NONE, - ); - match result { - Ok(_) => Ok(true), +pub async fn get_password(service: &str, account: &str) -> Result { + match get_password_new(service, account).await { + Ok(res) => Ok(res), Err(_) => { - println!("secret-service unavailable: {:?}", result); - Ok(false) + get_password_legacy(service, account).await } } } -fn get_schema() -> Schema { - let mut attributes = std::collections::HashMap::new(); - attributes.insert("service", libsecret::SchemaAttributeType::String); - attributes.insert("account", libsecret::SchemaAttributeType::String); - - libsecret::Schema::new( - "org.freedesktop.Secret.Generic", - libsecret::SchemaFlags::NONE, - attributes, - ) +async fn get_password_new(service: &str, account: &str) -> Result { + let keyring = oo7::Keyring::new().await?; + let attributes = HashMap::from([("service", service), ("account", account)]); + let results = keyring.search_items(&attributes).await?; + let res = results.get(0); + match res { + Some(res) => { + let secret = res.secret().await?; + Ok(String::from_utf8(secret.to_vec())?) + }, + None => Err(anyhow!("no result")) + } } -fn build_attributes<'a>(service: &'a str, account: &'a str) -> HashMap<&'a str, &'a str> { - let mut attributes = HashMap::new(); - attributes.insert("service", service); - attributes.insert("account", account); +// forces to read via secret service; remvove after 2025.03 +async fn get_password_legacy(service: &str, account: &str) -> Result { + println!("falling back to get legacy {} {}", service, account); + let svc = dbus::Service::new().await?; + let collection = svc.default_collection().await?; + let keyring = oo7::Keyring::DBus(collection); + let attributes = HashMap::from([("service", service), ("account", account)]); + let results = keyring.search_items(&attributes).await?; + let res = results.get(0); + match res { + Some(res) => { + let secret = res.secret().await?; + println!("deleting legacy secret service entry {} {}", service, account); + keyring.delete(&attributes).await?; + let secret_string = String::from_utf8(secret.to_vec())?; + set_password(service, account, &secret_string).await?; + Ok(secret_string) + }, + None => Err(anyhow!("no result")) + } +} - attributes +pub async fn set_password(service: &str, account: &str, password: &str) -> Result<()> { + let keyring = oo7::Keyring::new().await?; + let attributes = HashMap::from([("service", service), ("account", account)]); + keyring.create_item("org.freedesktop.Secret.Generic", &attributes, password, true).await?; + Ok(()) +} + +pub async fn delete_password(service: &str, account: &str) -> Result<()> { + let keyring = oo7::Keyring::new().await?; + let attributes = HashMap::from([("service", service), ("account", account)]); + keyring.delete(&attributes).await?; + Ok(()) +} + +pub async fn is_available() -> Result { + match oo7::Keyring::new().await { + Ok(_) => Ok(true), + _ => Ok(false), + } } #[cfg(test)] mod tests { use super::*; - #[test] - fn test() { - scopeguard::defer!(delete_password("BitwardenTest", "BitwardenTest").unwrap_or({});); - set_password("BitwardenTest", "BitwardenTest", "Random").unwrap(); + #[tokio::test] + async fn test() { + set_password("BitwardenTest", "BitwardenTest", "Random").await.unwrap(); assert_eq!( "Random", - get_password("BitwardenTest", "BitwardenTest").unwrap() + get_password("BitwardenTest", "BitwardenTest").await.unwrap() ); - delete_password("BitwardenTest", "BitwardenTest").unwrap(); + delete_password("BitwardenTest", "BitwardenTest").await.unwrap(); // Ensure password is deleted - match get_password("BitwardenTest", "BitwardenTest") { - Ok(_) => panic!("Got a result"), - Err(e) => assert_eq!("No password found", e.to_string()), + match get_password("BitwardenTest", "BitwardenTest").await { + Ok(_) => { + panic!("Got a result") + } + Err(e) => assert_eq!( + "no result", + e.to_string() + ), } } - #[test] - fn test_error_no_password() { - match get_password("BitwardenTest", "BitwardenTest") { + #[tokio::test] + async fn test_error_no_password() { + match get_password("Unknown", "Unknown").await { Ok(_) => panic!("Got a result"), - Err(e) => assert_eq!("No password found", e.to_string()), + Err(e) => assert_eq!( + "no result", + e.to_string() + ), } } } diff --git a/apps/desktop/desktop_native/core/src/password/windows.rs b/apps/desktop/desktop_native/core/src/password/windows.rs index d932aabae95..873e717ac8b 100644 --- a/apps/desktop/desktop_native/core/src/password/windows.rs +++ b/apps/desktop/desktop_native/core/src/password/windows.rs @@ -13,7 +13,7 @@ use windows::{ const CRED_FLAGS_NONE: u32 = 0; -pub fn get_password<'a>(service: &str, account: &str) -> Result { +pub async fn get_password<'a>(service: &str, account: &str) -> Result { let target_name = U16CString::from_str(target_name(service, account))?; let mut credential: *mut CREDENTIALW = std::ptr::null_mut(); @@ -45,39 +45,7 @@ pub fn get_password<'a>(service: &str, account: &str) -> Result { Ok(String::from(password)) } -// Remove this after sufficient releases -pub fn get_password_keytar<'a>(service: &str, account: &str) -> Result { - let target_name = U16CString::from_str(target_name(service, account))?; - - let mut credential: *mut CREDENTIALW = std::ptr::null_mut(); - let credential_ptr = &mut credential; - - let result = unsafe { - CredReadW( - PCWSTR(target_name.as_ptr()), - CRED_TYPE_GENERIC, - CRED_FLAGS_NONE, - credential_ptr, - ) - }; - - scopeguard::defer!({ - unsafe { CredFree(credential as *mut _) }; - }); - - result?; - - let password = unsafe { - std::str::from_utf8_unchecked(std::slice::from_raw_parts( - (*credential).CredentialBlob, - (*credential).CredentialBlobSize as usize, - )) - }; - - Ok(String::from(password)) -} - -pub fn set_password(service: &str, account: &str, password: &str) -> Result<()> { +pub async fn set_password(service: &str, account: &str, password: &str) -> Result<()> { let mut target_name = U16CString::from_str(target_name(service, account))?; let mut user_name = U16CString::from_str(account)?; let last_written = FILETIME { @@ -108,7 +76,7 @@ pub fn set_password(service: &str, account: &str, password: &str) -> Result<()> Ok(()) } -pub fn delete_password(service: &str, account: &str) -> Result<()> { +pub async fn delete_password(service: &str, account: &str) -> Result<()> { let target_name = U16CString::from_str(target_name(service, account))?; unsafe { @@ -122,7 +90,7 @@ pub fn delete_password(service: &str, account: &str) -> Result<()> { Ok(()) } -pub fn is_available() -> Result { +pub async fn is_available() -> Result { Ok(true) } @@ -142,36 +110,25 @@ fn convert_error(e: windows::core::Error) -> String { mod tests { use super::*; - #[test] - fn test() { - scopeguard::defer!(delete_password("BitwardenTest", "BitwardenTest").unwrap_or({});); - set_password("BitwardenTest", "BitwardenTest", "Random").unwrap(); + #[tokio::test] + async fn test() { + set_password("BitwardenTest", "BitwardenTest", "Random").await.unwrap(); assert_eq!( "Random", - get_password("BitwardenTest", "BitwardenTest").unwrap() + get_password("BitwardenTest", "BitwardenTest").await.unwrap() ); - delete_password("BitwardenTest", "BitwardenTest").unwrap(); + delete_password("BitwardenTest", "BitwardenTest").await.unwrap(); // Ensure password is deleted - match get_password("BitwardenTest", "BitwardenTest") { + match get_password("BitwardenTest", "BitwardenTest").await { Ok(_) => panic!("Got a result"), Err(e) => assert_eq!("Password not found.", e.to_string()), } } - #[test] - fn test_get_password_keytar() { - scopeguard::defer!(delete_password("BitwardenTest", "BitwardenTest").unwrap_or({});); - keytar::set_password("BitwardenTest", "BitwardenTest", "HelloFromKeytar").unwrap(); - assert_eq!( - "HelloFromKeytar", - get_password_keytar("BitwardenTest", "BitwardenTest").unwrap() - ); - } - - #[test] - fn test_error_no_password() { - match get_password("BitwardenTest", "BitwardenTest") { + #[tokio::test] + async fn test_error_no_password() { + match get_password("BitwardenTest", "BitwardenTest").await { Ok(_) => panic!("Got a result"), Err(e) => assert_eq!("Password not found.", e.to_string()), } diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index 644ff8a51d8..6efd662b351 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -19,7 +19,7 @@ hex = "=0.4.3" anyhow = "=1.0.93" desktop_core = { path = "../core" } napi = { version = "=2.16.13", features = ["async"] } -napi-derive = "=2.16.12" +napi-derive = "=2.16.13" tokio = { version = "=1.41.1" } tokio-util = "=0.7.12" tokio-stream = "=0.1.15" diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index debe2ccf1f4..87c2ddb6110 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -6,8 +6,6 @@ export declare namespace passwords { /** Fetch the stored password from the keychain. */ export function getPassword(service: string, account: string): Promise - /** Fetch the stored password from the keychain that was stored with Keytar. */ - export function getPasswordKeytar(service: string, account: string): Promise /** Save the password to the keychain. Adds an entry if none exists otherwise updates the existing entry. */ export function setPassword(service: string, account: string, password: string): Promise /** Delete the stored password from the keychain. */ @@ -132,3 +130,9 @@ export declare namespace epheremal_values { remove(key: string): void } } +export declare namespace autofill { + export function runCommand(value: string): Promise +} +export declare namespace crypto { + export function argon2(secret: Buffer, salt: Buffer, iterations: number, memory: number, parallelism: number): Promise +} diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 2a88fff9001..7919ce61ce9 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -9,13 +9,7 @@ pub mod passwords { #[napi] pub async fn get_password(service: String, account: String) -> napi::Result { desktop_core::password::get_password(&service, &account) - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - /// Fetch the stored password from the keychain that was stored with Keytar. - #[napi] - pub async fn get_password_keytar(service: String, account: String) -> napi::Result { - desktop_core::password::get_password_keytar(&service, &account) + .await .map_err(|e| napi::Error::from_reason(e.to_string())) } @@ -27,6 +21,7 @@ pub mod passwords { password: String, ) -> napi::Result<()> { desktop_core::password::set_password(&service, &account, &password) + .await .map_err(|e| napi::Error::from_reason(e.to_string())) } @@ -34,13 +29,16 @@ pub mod passwords { #[napi] pub async fn delete_password(service: String, account: String) -> napi::Result<()> { desktop_core::password::delete_password(&service, &account) + .await .map_err(|e| napi::Error::from_reason(e.to_string())) } // Checks if the os secure storage is available #[napi] pub async fn is_available() -> napi::Result { - desktop_core::password::is_available().map_err(|e| napi::Error::from_reason(e.to_string())) + desktop_core::password::is_available() + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) } } @@ -81,6 +79,7 @@ pub mod biometrics { key_material.map(|m| m.into()), &iv_b64, ) + .await .map_err(|e| napi::Error::from_reason(e.to_string())) } @@ -92,6 +91,7 @@ pub mod biometrics { ) -> napi::Result { let result = Biometric::get_biometric_secret(&service, &account, key_material.map(|m| m.into())) + .await .map_err(|e| napi::Error::from_reason(e.to_string())); result } @@ -249,13 +249,17 @@ pub mod sshagent { pub async fn serve( callback: ThreadsafeFunction<(String, bool), CalleeHandled>, ) -> napi::Result { - let (auth_request_tx, mut auth_request_rx) = tokio::sync::mpsc::channel::<(u32, (String, bool))>(32); - let (auth_response_tx, auth_response_rx) = tokio::sync::broadcast::channel::<(u32, bool)>(32); + let (auth_request_tx, mut auth_request_rx) = + tokio::sync::mpsc::channel::<(u32, (String, bool))>(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, is_list_request))) = auth_request_rx.recv().await { + while let Some((request_id, (cipher_uuid, is_list_request))) = + 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(); @@ -265,23 +269,33 @@ pub mod sshagent { 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, is_list_request))).await; + let promise_result: Result, napi::Error> = callback + .call_async(Ok((cipher_uuid, is_list_request))) + .await; match promise_result { Ok(promise_result) => match promise_result.await { Ok(result) => { - let _ = auth_response_tx_arc.lock().await.send((request_id, 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)) + 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)) + let _ = auth_response_tx_arc + .lock() + .await + .send((request_id, false)) .expect("should be able to send auth response to agent"); } } @@ -348,7 +362,9 @@ pub mod sshagent { #[napi] pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> { let bitwarden_agent_state = &mut agent_state.state; - bitwarden_agent_state.clear_keys().map_err(|e| napi::Error::from_reason(e.to_string())) + bitwarden_agent_state + .clear_keys() + .map_err(|e| napi::Error::from_reason(e.to_string())) } #[napi] @@ -564,3 +580,30 @@ pub mod epheremal_values { } } } +pub mod autofill { + #[napi] + pub async fn run_command(value: String) -> napi::Result { + desktop_core::autofill::run_command(value) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod crypto { + use napi::bindgen_prelude::Buffer; + + #[napi] + pub async fn argon2( + secret: Buffer, + salt: Buffer, + iterations: u32, + memory: u32, + parallelism: u32, + ) -> napi::Result { + desktop_core::crypto::argon2(&secret, &salt, iterations, memory, parallelism) + .map_err(|e| napi::Error::from_reason(e.to_string())) + .map(|v| v.to_vec()) + .map(|v| Buffer::from(v)) + } +} diff --git a/apps/desktop/desktop_native/objc/Cargo.toml b/apps/desktop/desktop_native/objc/Cargo.toml new file mode 100644 index 00000000000..4b883ce6fa7 --- /dev/null +++ b/apps/desktop/desktop_native/objc/Cargo.toml @@ -0,0 +1,21 @@ +[package] +edition = "2021" +license = "GPL-3.0" +name = "desktop_objc" +version = "0.0.0" +publish = false + +[features] +default = [] + +[dependencies] +anyhow = "=1.0.93" +thiserror = "=1.0.69" +tokio = "1.39.1" + +[target.'cfg(target_os = "macos")'.dependencies] +core-foundation = "=0.9.4" + +[build-dependencies] +cc = "1.0.104" +glob = "0.3.1" diff --git a/apps/desktop/desktop_native/objc/build.rs b/apps/desktop/desktop_native/objc/build.rs new file mode 100644 index 00000000000..57f3b626bf7 --- /dev/null +++ b/apps/desktop/desktop_native/objc/build.rs @@ -0,0 +1,22 @@ +use glob::glob; + +#[cfg(target_os = "macos")] +fn main() { + let mut builder = cc::Build::new(); + + // Auto compile all .m files in the src/native directory + for entry in glob("src/native/**/*.m").expect("Failed to read glob pattern") { + let path = entry.expect("Failed to read glob entry"); + builder.file(path.clone()); + println!("cargo::rerun-if-changed={}", path.display()); + } + + builder + .flag("-fobjc-arc") // Enable Auto Reference Counting (ARC) + .compile("autofill"); +} + +#[cfg(not(target_os = "macos"))] +fn main() { + // Crate is only supported on macOS +} diff --git a/apps/desktop/desktop_native/objc/src/lib.rs b/apps/desktop/desktop_native/objc/src/lib.rs new file mode 100644 index 00000000000..64475b7956d --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/lib.rs @@ -0,0 +1,124 @@ +#![cfg(target_os = "macos")] + +use std::{ + ffi::{c_char, CStr, CString}, + os::raw::c_void, +}; + +use anyhow::{Context, Result}; + +#[repr(C)] +pub struct ObjCString { + value: *const c_char, + size: usize, +} + +#[repr(C)] +pub struct CommandContext { + tx: Option>, +} + +impl CommandContext { + pub fn new() -> (Self, tokio::sync::oneshot::Receiver) { + let (tx, rx) = tokio::sync::oneshot::channel::(); + + (CommandContext { tx: Some(tx) }, rx) + } + + pub fn send(&mut self, value: String) -> Result<()> { + let tx = self.tx.take().context( + "Failed to take Sender from CommandContext. Has this context already returned once?", + )?; + + tx.send(value).map_err(|_| { + anyhow::anyhow!("Failed to send ObjCString from CommandContext to Rust code") + })?; + + Ok(()) + } + + pub fn as_ptr(&mut self) -> *mut c_void { + self as *mut Self as *mut c_void + } +} + +impl TryFrom for String { + type Error = anyhow::Error; + + fn try_from(value: ObjCString) -> Result { + let c_str = unsafe { CStr::from_ptr(value.value) }; + let str = c_str + .to_str() + .context("Failed to convert ObjC output string to &str for use in Rust")?; + + Ok(str.to_owned()) + } +} + +impl Drop for ObjCString { + fn drop(&mut self) { + unsafe { + objc::freeObjCString(self); + } + } +} + +mod objc { + use std::os::raw::c_void; + + use super::*; + + extern "C" { + pub fn runCommand(context: *mut c_void, value: *const c_char); + pub fn freeObjCString(value: &ObjCString); + } + + /// This function is called from the ObjC code to return the output of the command + #[no_mangle] + pub extern "C" fn commandReturn(context: &mut CommandContext, value: ObjCString) -> bool { + let value: String = match value.try_into() { + Ok(value) => value, + Err(e) => { + println!( + "Error: Failed to convert ObjCString to Rust string during commandReturn: {}", + e + ); + + return false; + } + }; + + match context.send(value) { + Ok(_) => 0, + Err(e) => { + println!( + "Error: Failed to return ObjCString from ObjC code to Rust code: {}", + e + ); + + return false; + } + }; + + return true; + } +} + +pub async fn run_command(input: String) -> Result { + // Convert input to type that can be passed to ObjC code + let c_input = CString::new(input) + .context("Failed to convert Rust input string to a CString for use in call to ObjC code")?; + + let (mut context, rx) = CommandContext::new(); + + // Call ObjC code + unsafe { objc::runCommand(context.as_ptr(), c_input.as_ptr()) }; + + // Convert output from ObjC code to Rust string + let objc_output = rx.await?.try_into()?; + + // Convert output from ObjC code to Rust string + // let objc_output = output.try_into()?; + + Ok(objc_output) +} diff --git a/apps/desktop/desktop_native/objc/src/native/.clangd b/apps/desktop/desktop_native/objc/src/native/.clangd new file mode 100644 index 00000000000..5369bebcdc0 --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Add: [-fobjc-arc] diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/commands/status.h b/apps/desktop/desktop_native/objc/src/native/autofill/commands/status.h new file mode 100644 index 00000000000..e3e3c7969b6 --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/autofill/commands/status.h @@ -0,0 +1,8 @@ +#ifndef STATUS_H +#define STATUS_H + +#import + +void status(void *context, NSDictionary *params); + +#endif diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/commands/status.m b/apps/desktop/desktop_native/objc/src/native/autofill/commands/status.m new file mode 100644 index 00000000000..8811ffc6f0c --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/autofill/commands/status.m @@ -0,0 +1,57 @@ +#import +#import +#import +#import "../../interop.h" +#import "status.h" + +void storeState(void (^callback)(ASCredentialIdentityStoreState*)) { + if (@available(macos 11, *)) { + ASCredentialIdentityStore *store = [ASCredentialIdentityStore sharedStore]; + [store getCredentialIdentityStoreStateWithCompletion:^(ASCredentialIdentityStoreState * _Nonnull state) { + callback(state); + }]; + } else { + callback(nil); + } +} + +BOOL fido2Supported() { + if (@available(macos 14, *)) { + return YES; + } else { + return NO; + } +} + +BOOL passwordSupported() { + if (@available(macos 11, *)) { + return YES; + } else { + return NO; + } +} + +void status(void* context, __attribute__((unused)) NSDictionary *params) { + storeState(^(ASCredentialIdentityStoreState *state) { + BOOL enabled = NO; + BOOL supportsIncremental = NO; + + if (state != nil) { + enabled = state.isEnabled; + supportsIncremental = state.supportsIncrementalUpdates; + } + + _return(context, + _success(@{ + @"support": @{ + @"fido2": @(fido2Supported()), + @"password": @(passwordSupported()), + @"incrementalUpdates": @(supportsIncremental), + }, + @"state": @{ + @"enabled": @(enabled), + } + }) + ); + }); +} diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.h b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.h new file mode 100644 index 00000000000..4eb39ff24d6 --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.h @@ -0,0 +1,8 @@ +#ifndef SYNC_H +#define SYNC_H + +#import + +void runSync(void *context, NSDictionary *params); + +#endif diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m new file mode 100644 index 00000000000..8b73635a7ca --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m @@ -0,0 +1,59 @@ +#import +#import +#import +#import +#import +#import +#import "../../utils.h" +#import "../../interop.h" +#import "sync.h" + +// 'run' is added to the name because it clashes with internal macOS function +void runSync(void* context, NSDictionary *params) { + NSArray *credentials = params[@"credentials"]; + + // Map credentials to ASPasswordCredential objects + NSMutableArray *mappedCredentials = [NSMutableArray arrayWithCapacity:credentials.count]; + for (NSDictionary *credential in credentials) { + NSString *type = credential[@"type"]; + + if ([type isEqualToString:@"password"]) { + NSString *cipherId = credential[@"cipherId"]; + NSString *uri = credential[@"uri"]; + NSString *username = credential[@"username"]; + + ASCredentialServiceIdentifier *serviceId = [[ASCredentialServiceIdentifier alloc] + initWithIdentifier:uri type:ASCredentialServiceIdentifierTypeURL]; + ASPasswordCredentialIdentity *credential = [[ASPasswordCredentialIdentity alloc] + initWithServiceIdentifier:serviceId user:username recordIdentifier:cipherId]; + + [mappedCredentials addObject:credential]; + } + + if ([type isEqualToString:@"fido2"]) { + NSString *cipherId = credential[@"cipherId"]; + NSString *rpId = credential[@"rpId"]; + NSString *userName = credential[@"userName"]; + NSData *credentialId = decodeBase64URL(credential[@"credentialId"]); + NSData *userHandle = decodeBase64URL(credential[@"userHandle"]); + + ASPasskeyCredentialIdentity *credential = [[ASPasskeyCredentialIdentity alloc] + initWithRelyingPartyIdentifier:rpId + userName:userName + credentialID:credentialId + userHandle:userHandle + recordIdentifier:cipherId]; + + [mappedCredentials addObject:credential]; + } + } + + [ASCredentialIdentityStore.sharedStore replaceCredentialIdentityEntries:mappedCredentials + completion:^(__attribute__((unused)) BOOL success, NSError * _Nullable error) { + if (error) { + return _return(context, _error_er(error)); + } + + _return(context, _success(@{@"added": @([mappedCredentials count])})); + }]; +} diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/run_autofill_command.h b/apps/desktop/desktop_native/objc/src/native/autofill/run_autofill_command.h new file mode 100644 index 00000000000..fba5f626863 --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/autofill/run_autofill_command.h @@ -0,0 +1,8 @@ +#ifndef RUN_AUTOFILL_COMMAND_H +#define RUN_AUTOFILL_COMMAND_H + +#import + +void runAutofillCommand(void* context, NSDictionary *input); + +#endif diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/run_autofill_command.m b/apps/desktop/desktop_native/objc/src/native/autofill/run_autofill_command.m new file mode 100644 index 00000000000..46b188139bc --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/autofill/run_autofill_command.m @@ -0,0 +1,20 @@ +#import +#import "commands/sync.h" +#import "commands/status.h" +#import "../interop.h" +#import "../utils.h" +#import "run_autofill_command.h" + +void runAutofillCommand(void* context, NSDictionary *input) { + NSString *command = input[@"command"]; + NSDictionary *params = input[@"params"]; + + if ([command isEqual:@"status"]) { + return status(context, params); + } else if ([command isEqual:@"sync"]) { + return runSync(context, params); + } + + _return(context, _error([NSString stringWithFormat:@"Unknown command: %@", command])); +} + diff --git a/apps/desktop/desktop_native/objc/src/native/interop.h b/apps/desktop/desktop_native/objc/src/native/interop.h new file mode 100644 index 00000000000..584fe547a9d --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/interop.h @@ -0,0 +1,47 @@ +#ifndef INTEROP_H +#define INTEROP_H + +#import + +// Tips for developing Objective-C code: +// - Use the `NSLog` function to log messages to the system log +// - Example: +// NSLog(@"An example log: %@", someVariable); +// - Use the `@try` and `@catch` directives to catch exceptions + +#if !__has_feature(objc_arc) + // Auto Reference Counting makes memory management easier for Objective-C objects + // Regular C objects still need to be managed manually + #error ARC must be enabled! +#endif + +/// [Shared with Rust] +/// Simple struct to hold a C-string and its length +/// This is used to return strings created in Objective-C to Rust +/// so that Rust can free the memory when it's done with the string +struct ObjCString +{ + char *value; + size_t size; +}; + +/// [Defined in Rust] +/// External function callable from Objective-C to return a string to Rust +extern bool commandReturn(void *context, struct ObjCString output); + +/// [Callable from Rust] +/// Frees the memory allocated for an ObjCString +void freeObjCString(struct ObjCString *value); + +// --- Helper functions to convert between Objective-C and Rust types --- + +NSString *_success(NSDictionary *value); +NSString *_error(NSString *error); +NSString *_error_er(NSError *error); +NSString *_error_ex(NSException *error); +void _return(void *context, NSString *output); + +struct ObjCString nsStringToObjCString(NSString *string); +NSString *cStringToNSString(char *string); + +#endif diff --git a/apps/desktop/desktop_native/objc/src/native/interop.m b/apps/desktop/desktop_native/objc/src/native/interop.m new file mode 100644 index 00000000000..dc41eb52d76 --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/interop.m @@ -0,0 +1,71 @@ +#import "interop.h" +#import "utils.h" + +/// [Callable from Rust] +/// Frees the memory allocated for an ObjCString +void freeObjCString(struct ObjCString *value) { + free(value->value); +} + +// --- Helper functions to convert between Objective-C and Rust types --- + +NSString *_success(NSDictionary *value) { + NSDictionary *wrapper = @{@"type": @"success", @"value": value}; + NSError *jsonError = nil; + NSString *toReturn = serializeJson(wrapper, jsonError); + + if (jsonError) { + // Manually format message since there seems to be an issue with the JSON serialization + return [NSString stringWithFormat:@"{\"type\": \"error\", \"error\": \"Error occurred while serializing error: %@\"}", jsonError]; + } + + return toReturn; +} + +NSString *_error(NSString *error) { + NSDictionary *errorDictionary = @{@"type": @"error", @"error": error}; + NSError *jsonError = nil; + NSString *toReturn = serializeJson(errorDictionary, jsonError); + + if (jsonError) { + // Manually format message since there seems to be an issue with the JSON serialization + return [NSString stringWithFormat:@"{\"type\": \"error\", \"error\": \"Error occurred while serializing error: %@\"}", jsonError]; + } + + return toReturn; +} + +NSString *_error_er(NSError *error) { + return _error([error localizedDescription]); +} + +NSString *_error_ex(NSException *error) { + return _error([NSString stringWithFormat:@"%@ (%@): %@", error.name, error.reason, [error callStackSymbols]]); +} + +void _return(void* context, NSString *output) { + if (!commandReturn(context, nsStringToObjCString(output))) { + NSLog(@"Error: Failed to return command output"); + // NOTE: This will most likely crash the application + @throw [NSException exceptionWithName:@"CommandReturnError" reason:@"Failed to return command output" userInfo:nil]; + } +} + +/// Converts an NSString to an ObjCString struct +struct ObjCString nsStringToObjCString(NSString* string) { + size_t size = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1; + char *value = malloc(size); + [string getCString:value maxLength:size encoding:NSUTF8StringEncoding]; + + struct ObjCString objCString; + objCString.value = value; + objCString.size = size; + + return objCString; +} + +/// Converts a C-string to an NSString +NSString* cStringToNSString(char* string) { + return [[NSString alloc] initWithUTF8String:string]; +} + diff --git a/apps/desktop/desktop_native/objc/src/native/run_command.m b/apps/desktop/desktop_native/objc/src/native/run_command.m new file mode 100644 index 00000000000..eb90a3340db --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/run_command.m @@ -0,0 +1,39 @@ +#import +#import "autofill/run_autofill_command.h" +#import "interop.h" +#import "utils.h" + +void pickAndRunCommand(void* context, NSDictionary *input) { + NSString *namespace = input[@"namespace"]; + + if ([namespace isEqual:@"autofill"]) { + return runAutofillCommand(context, input); + } + + _return(context, _error([NSString stringWithFormat:@"Unknown namespace: %@", namespace])); +} + +/// [Callable from Rust] +/// Runs a command with the given input JSON +/// This function is called from Rust and is the entry point for running Objective-C code. +/// It takes a JSON string as input, deserializes it, runs the command, and serializes the output. +/// It also catches any exceptions that occur during the command execution. +void runCommand(void *context, char* inputJson) { + @autoreleasepool { + @try { + NSString *inputString = cStringToNSString(inputJson); + + NSError *error = nil; + NSDictionary *input = parseJson(inputString, error); + if (error) { + NSLog(@"Error occured while deserializing input params: %@", error); + return _return(context, _error([NSString stringWithFormat:@"Error occured while deserializing input params: %@", error])); + } + + pickAndRunCommand(context, input); + } @catch (NSException *e) { + NSLog(@"Error occurred while running Objective-C command: %@", e); + _return(context, _error([NSString stringWithFormat:@"Error occurred while running Objective-C command: %@", e])); + } + } +} diff --git a/apps/desktop/desktop_native/objc/src/native/utils.h b/apps/desktop/desktop_native/objc/src/native/utils.h new file mode 100644 index 00000000000..50fc991d15f --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/utils.h @@ -0,0 +1,11 @@ +#ifndef UTILS_H +#define UTILS_H + +#import + +NSDictionary *parseJson(NSString *jsonString, NSError *error); +NSString *serializeJson(NSDictionary *dictionary, NSError *error); + +NSData *decodeBase64URL(NSString *base64URLString); + +#endif diff --git a/apps/desktop/desktop_native/objc/src/native/utils.m b/apps/desktop/desktop_native/objc/src/native/utils.m new file mode 100644 index 00000000000..040c723a8ac --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/utils.m @@ -0,0 +1,28 @@ +#import "utils.h" + +NSDictionary *parseJson(NSString *jsonString, NSError *error) { + NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + if (error) { + return nil; + } + return json; +} + +NSString *serializeJson(NSDictionary *dictionary, NSError *error) { + NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:&error]; + if (error) { + return nil; + } + return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; +} + +NSData *decodeBase64URL(NSString *base64URLString) { + NSString *base64String = [base64URLString stringByReplacingOccurrencesOfString:@"-" withString:@"+"]; + base64String = [base64String stringByReplacingOccurrencesOfString:@"_" withString:@"/"]; + + NSData *nsdataFromBase64String = [[NSData alloc] + initWithBase64EncodedString:base64String options:0]; + + return nsdataFromBase64String; +} diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 9a8bc45ae20..9b894b0bfc7 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -18,12 +18,7 @@ "**/*", "!**/node_modules/@bitwarden/desktop-napi/**/*", "**/node_modules/@bitwarden/desktop-napi/index.js", - "**/node_modules/@bitwarden/desktop-napi/desktop_napi.${platform}-${arch}*.node", - - "!**/node_modules/argon2/**/*", - "**/node_modules/argon2/argon2.cjs", - "**/node_modules/argon2/package.json", - "**/node_modules/argon2/build/Release/argon2.node" + "**/node_modules/@bitwarden/desktop-napi/desktop_napi.${platform}-${arch}*.node" ], "electronVersion": "32.1.1", "generateUpdatesFilesForAllChannels": true, @@ -229,7 +224,7 @@ }, "deb": { "artifactName": "${productName}-${version}-${arch}.${ext}", - "depends": ["libnotify4", "libxtst6", "libnss3", "libsecret-1-0", "libxss1"] + "depends": ["libnotify4", "libxtst6", "libnss3", "libxss1"] }, "appImage": { "artifactName": "${productName}-${version}-${arch}.${ext}" diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index d5c5cabeee4..4d03bf97e6c 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -19,17 +19,18 @@ class CredentialProviderViewController: ASCredentialProviderViewController { If using the credential would require showing custom UI for authenticating the user, cancel the request with error code ASExtensionError.userInteractionRequired. - override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) { - let databaseIsUnlocked = true - if (databaseIsUnlocked) { - let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234") - self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) - } else { - self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code:ASExtensionError.userInteractionRequired.rawValue)) - } - } - */ + */ + override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) { +// let databaseIsUnlocked = true +// if (databaseIsUnlocked) { + let passwordCredential = ASPasswordCredential(user: credentialIdentity.user, password: "example1234") + self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) +// } else { +// self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code:ASExtensionError.userInteractionRequired.rawValue)) +// } + } + /* Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with ASExtensionError.userInteractionRequired. In this case, the system may present your extension's diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 54feb7df9e6..eab9a7d7119 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -48,6 +48,7 @@ "dist:mac": "npm run build && npm run pack:mac", "dist:mac:mas": "npm run build && npm run pack:mac:mas", "dist:mac:masdev": "npm run build && npm run pack:mac:masdev", + "dist:mac:masdev:with-extension": "npm run build && npm run pack:mac:masdev:with-extension", "dist:win": "npm run build && npm run pack:win", "dist:win:ci": "npm run build && npm run pack:win:ci", "publish:lin": "npm run build && npm run clean:dist && electron-builder --linux --x64 -p always", diff --git a/apps/desktop/scripts/build-macos-extension.js b/apps/desktop/scripts/build-macos-extension.js index 3aa43fb6785..4cb643c34d4 100644 --- a/apps/desktop/scripts/build-macos-extension.js +++ b/apps/desktop/scripts/build-macos-extension.js @@ -28,8 +28,9 @@ async function buildMacOs() { "-alltargets", "-configuration", "Release", - "-xcconfig", - paths.macOsConfig, + // Uncomment when signing is fixed + // "-xcconfig", + // paths.macOsConfig, ]); stdOutProc(proc); await new Promise((resolve, reject) => diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index e61335859c4..db9ece317c8 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -5,6 +5,7 @@ import { DesktopDefaultOverlayPosition, EnvironmentSelectorComponent, } from "@bitwarden/angular/auth/components/environment-selector.component"; +import { TwoFactorTimeoutComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-expired.component"; import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap"; import { authGuard, @@ -35,6 +36,7 @@ import { VaultIcon, LoginDecryptionOptionsComponent, DevicesIcon, + TwoFactorTimeoutIcon, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -96,6 +98,22 @@ const routes: Routes = [ ], }, ), + { + path: "2fa-timeout", + component: AnonLayoutWrapperComponent, + children: [ + { + path: "", + component: TwoFactorTimeoutComponent, + }, + ], + data: { + pageIcon: TwoFactorTimeoutIcon, + pageTitle: { + key: "authenticationTimeout", + }, + } satisfies RouteDataProperties & AnonLayoutWrapperData, + }, { path: "register", component: RegisterComponent }, { path: "vault", diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index 0068a03d685..de80f95593a 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -20,6 +20,7 @@ import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/va import { UserId } from "@bitwarden/common/types/guid"; import { KeyService as KeyServiceAbstraction } from "@bitwarden/key-management"; +import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service"; import { I18nRendererService } from "../../platform/services/i18n.renderer.service"; import { SshAgentService } from "../../platform/services/ssh-agent.service"; import { VersionService } from "../../platform/services/version.service"; @@ -45,6 +46,7 @@ export class InitService { private accountService: AccountService, private versionService: VersionService, private sshAgentService: SshAgentService, + private autofillService: DesktopAutofillService, @Inject(DOCUMENT) private document: Document, ) {} @@ -82,6 +84,8 @@ export class InitService { const containerService = new ContainerService(this.keyService, this.encryptService); containerService.attachToGlobal(this.win); + + await this.autofillService.init(); }; } } diff --git a/apps/desktop/src/app/services/renderer-crypto-function.service.ts b/apps/desktop/src/app/services/renderer-crypto-function.service.ts index 604e70b1ebd..ee80dc25933 100644 --- a/apps/desktop/src/app/services/renderer-crypto-function.service.ts +++ b/apps/desktop/src/app/services/renderer-crypto-function.service.ts @@ -19,6 +19,13 @@ export class RendererCryptoFunctionService memory: number, parallelism: number, ): Promise { + if (typeof password === "string") { + password = new TextEncoder().encode(password); + } + if (typeof salt === "string") { + salt = new TextEncoder().encode(salt); + } + return await ipc.platform.crypto.argon2(password, salt, iterations, memory, parallelism); } } diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 36f1b318609..30ed6d26a00 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -51,6 +51,7 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s import { ClientType } from "@bitwarden/common/enums"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -95,6 +96,7 @@ import { DesktopLoginApprovalComponentService } from "../../auth/login/desktop-l import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service"; import { DesktopPinService } from "../../auth/services/desktop-pin.service"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; +import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service"; import { ElectronBiometricsService } from "../../key-management/biometrics/electron-biometrics.service"; import { flagEnabled } from "../../platform/flags"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; @@ -307,6 +309,10 @@ const safeProviders: SafeProvider[] = [ provide: DesktopAutofillSettingsService, deps: [StateProvider], }), + safeProvider({ + provide: DesktopAutofillService, + deps: [LogService, CipherServiceAbstraction, ConfigService], + }), safeProvider({ provide: NativeMessagingManifestService, useClass: NativeMessagingManifestService, diff --git a/apps/desktop/src/app/tools/generator/credential-generator.component.html b/apps/desktop/src/app/tools/generator/credential-generator.component.html index 37d677472d9..66b0de13dfb 100644 --- a/apps/desktop/src/app/tools/generator/credential-generator.component.html +++ b/apps/desktop/src/app/tools/generator/credential-generator.component.html @@ -6,6 +6,7 @@
- - + diff --git a/apps/web/src/app/tools/credential-generator/credential-generator.component.ts b/apps/web/src/app/tools/credential-generator/credential-generator.component.ts index f252796d062..8d7b56a09ad 100644 --- a/apps/web/src/app/tools/credential-generator/credential-generator.component.ts +++ b/apps/web/src/app/tools/credential-generator/credential-generator.component.ts @@ -1,6 +1,6 @@ import { Component } from "@angular/core"; -import { ButtonModule, DialogService, ItemModule, LinkModule } from "@bitwarden/components"; +import { ButtonModule, DialogService, LinkModule } from "@bitwarden/components"; import { CredentialGeneratorHistoryDialogComponent, GeneratorModule, @@ -13,7 +13,7 @@ import { SharedModule } from "../../shared"; standalone: true, selector: "credential-generator", templateUrl: "credential-generator.component.html", - imports: [SharedModule, HeaderModule, GeneratorModule, ItemModule, ButtonModule, LinkModule], + imports: [SharedModule, HeaderModule, GeneratorModule, ButtonModule, LinkModule], }) export class CredentialGeneratorComponent { constructor(private dialogService: DialogService) {} 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 2742cd52ef1..2f5b014fcb3 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 @@ -281,17 +281,19 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { // If the cipher was newly created (via add/clone), switch the form to edit for subsequent edits. if (this._originalFormMode === "add" || this._originalFormMode === "clone") { this.formConfig.mode = "edit"; + this.formConfig.initialValues = null; } - let cipher: Cipher; + let cipher = await this.cipherService.get(cipherView.id); - // When the form config is used within the Admin Console, retrieve the cipher from the admin endpoint - if (this.formConfig.isAdminConsole) { + // When the form config is used within the Admin Console, retrieve the cipher from the admin endpoint (if not found in local state) + if (this.formConfig.isAdminConsole && (cipher == null || this.formConfig.admin)) { const cipherResponse = await this.apiService.getCipherAdmin(cipherView.id); + cipherResponse.edit = true; + cipherResponse.viewPassword = true; + 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 diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.html b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.html index f3ecead886b..abb4d19c9d3 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.html +++ b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.html @@ -9,7 +9,7 @@ {{ freeTrialMessage(organization) }} { @@ -258,13 +261,16 @@ export class VaultFilterComponent implements OnInit, OnDestroy { type: CipherType.SecureNote, icon: "bwi-sticky-note", }, - { + ]; + + if (await this.configService.getFeatureFlag(FeatureFlag.SSHKeyVaultItem)) { + allTypeFilters.push({ id: "sshKey", name: this.i18nService.t("typeSshKey"), type: CipherType.SshKey, icon: "bwi-key", - }, - ]; + }); + } const typeFilterSection: VaultFilterSection = { data$: this.vaultFilterService.buildTypeTree( diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html index 679d2ce6f7e..75332dcf72a 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -1,4 +1,6 @@ - + ; private activeUserId: UserId; - protected organizationsPaymentStatus: FreeTrial[] = []; private searchText$ = new Subject(); private refresh$ = new BehaviorSubject(null); private destroy$ = new Subject(); @@ -208,6 +208,40 @@ export class VaultComponent implements OnInit, OnDestroy { ), ); + protected organizationsPaymentStatus$: Observable = combineLatest([ + this.organizationService.organizations$.pipe( + map( + (organizations) => + organizations?.filter((org) => org.isOwner && org.canViewBillingHistory) ?? [], + ), + ), + this.hasSubscription$, + ]).pipe( + switchMap(([ownerOrgs, hasSubscription]) => { + if (!ownerOrgs || ownerOrgs.length === 0 || !hasSubscription) { + return of([]); + } + return combineLatest( + ownerOrgs.map((org) => + combineLatest([ + this.organizationApiService.getSubscription(org.id), + this.organizationBillingService.getPaymentSource(org.id), + ]).pipe( + map(([subscription, paymentSource]) => { + return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( + org, + subscription, + paymentSource, + ); + }), + ), + ), + ); + }), + map((results) => results.filter((result) => result.shownBanner)), + shareReplay({ refCount: false, bufferSize: 1 }), + ); + constructor( private syncService: SyncService, private route: ActivatedRoute, @@ -241,6 +275,7 @@ export class VaultComponent implements OnInit, OnDestroy { private organizationApiService: OrganizationApiServiceAbstraction, protected billingApiService: BillingApiServiceAbstraction, private trialFlowService: TrialFlowService, + private organizationBillingService: OrganizationBillingServiceAbstraction, ) {} async ngOnInit() { @@ -423,36 +458,6 @@ export class VaultComponent implements OnInit, OnDestroy { this.unpaidSubscriptionDialog$.pipe(takeUntil(this.destroy$)).subscribe(); - const organizationsPaymentStatus$ = combineLatest([ - this.organizationService.organizations$, - this.hasSubscription$, - ]).pipe( - switchMap(([allOrganizations, hasSubscription]) => { - if (!allOrganizations || allOrganizations.length === 0 || !hasSubscription) { - return of([]); - } - return combineLatest( - allOrganizations - .filter((org) => org.isOwner && hasSubscription) - .map((org) => - combineLatest([ - this.organizationApiService.getSubscription(org.id), - this.organizationApiService.getBilling(org.id), - ]).pipe( - map(([subscription, billing]) => { - return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( - org, - subscription, - billing?.paymentSource, - ); - }), - ), - ), - ); - }), - map((results) => results.filter((result) => result.shownBanner)), - ); - firstSetup$ .pipe( switchMap(() => this.refresh$), @@ -466,7 +471,6 @@ export class VaultComponent implements OnInit, OnDestroy { ciphers$, collections$, selectedCollection$, - organizationsPaymentStatus$, ]), ), takeUntil(this.destroy$), @@ -480,7 +484,6 @@ export class VaultComponent implements OnInit, OnDestroy { ciphers, collections, selectedCollection, - organizationsPaymentStatus, ]) => { this.filter = filter; this.canAccessPremium = canAccessPremium; @@ -496,7 +499,6 @@ export class VaultComponent implements OnInit, OnDestroy { this.showBulkMove = filter.type !== "trash"; this.isEmpty = collections?.length === 0 && ciphers?.length === 0; - this.organizationsPaymentStatus = organizationsPaymentStatus; this.performingInitialLoad = false; this.refreshing = false; }, 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 05c40fe2e79..25976c4fb82 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 @@ -8,6 +8,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli 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"; @@ -52,12 +53,16 @@ describe("AdminConsoleCipherFormConfigService", () => { 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(); getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" }); - await TestBed.configureTestingModule({ + getCipher.mockClear(); + getCipher.mockResolvedValue({ id: cipherId, name: "Test Cipher" }); + + TestBed.configureTestingModule({ providers: [ AdminConsoleCipherFormConfigService, { provide: OrganizationService, useValue: { get$: () => organization$, organizations$ } }, @@ -74,14 +79,14 @@ describe("AdminConsoleCipherFormConfigService", () => { useValue: { filter$: new BehaviorSubject({ organizationId: testOrg.id }) }, }, { provide: ApiService, useValue: { getCipherAdmin } }, + { provide: CipherService, useValue: { get: getCipher } }, ], }); + adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); }); describe("buildConfig", () => { it("sets individual attributes", async () => { - adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); - const { folders, hideIndividualVaultFields } = await adminConsoleConfigService.buildConfig( "add", cipherId, @@ -92,8 +97,6 @@ describe("AdminConsoleCipherFormConfigService", () => { }); it("sets mode based on passed mode", async () => { - adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); - const { mode } = await adminConsoleConfigService.buildConfig("edit", cipherId); expect(mode).toBe("edit"); @@ -122,8 +125,6 @@ describe("AdminConsoleCipherFormConfigService", () => { }); it("sets `allowPersonalOwnership`", async () => { - adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); - policyAppliesToActiveUser$.next(true); let result = await adminConsoleConfigService.buildConfig("clone", cipherId); @@ -138,8 +139,6 @@ describe("AdminConsoleCipherFormConfigService", () => { }); it("disables personal ownership when not cloning", async () => { - adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); - policyAppliesToActiveUser$.next(false); let result = await adminConsoleConfigService.buildConfig("add", cipherId); @@ -172,14 +171,32 @@ describe("AdminConsoleCipherFormConfigService", () => { expect(result.organizations).toEqual([testOrg, testOrg2]); }); - it("retrieves the cipher from the admin service", async () => { + it("retrieves the cipher from the admin service when canEditAllCiphers is true", async () => { getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" }); + testOrg.canEditAllCiphers = true; - adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); - - await adminConsoleConfigService.buildConfig("add", cipherId); + await adminConsoleConfigService.buildConfig("edit", cipherId); expect(getCipherAdmin).toHaveBeenCalledWith(cipherId); }); + + it("retrieves the cipher from the admin service when not found in local state", async () => { + getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" }); + testOrg.canEditAllCiphers = false; + getCipher.mockResolvedValue(null); + + await adminConsoleConfigService.buildConfig("edit", cipherId); + + expect(getCipherAdmin).toHaveBeenCalledWith(cipherId); + }); + + it("retrieves the cipher from local state when admin is not required", async () => { + testOrg.canEditAllCiphers = false; + + await adminConsoleConfigService.buildConfig("edit", cipherId); + + expect(getCipherAdmin).not.toHaveBeenCalled(); + expect(getCipher).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 457b4e83d03..50439a4d8dc 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 @@ -5,8 +5,10 @@ import { CollectionAdminService } 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 { PolicyType, OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; +import { OrganizationUserStatusType, PolicyType } 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,6 +27,7 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ private organizationService: OrganizationService = inject(OrganizationService); private routedVaultFilterService: RoutedVaultFilterService = inject(RoutedVaultFilterService); private collectionAdminService: CollectionAdminService = inject(CollectionAdminService); + private cipherService: CipherService = inject(CipherService); private apiService: ApiService = inject(ApiService); private allowPersonalOwnership$ = this.policyService @@ -57,7 +60,6 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ cipherId?: CipherId, cipherType?: CipherType, ): Promise { - const cipher = await this.getCipher(cipherId); const [organization, allowPersonalOwnership, allOrganizations, allCollections] = await firstValueFrom( combineLatest([ @@ -74,7 +76,7 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ // Only allow the user to assign to their personal vault when cloning and // the policies are enabled for it. const allowPersonalOwnershipOnlyForClone = mode === "clone" ? allowPersonalOwnership : false; - + const cipher = await this.getCipher(cipherId, organization); return { mode, cipherType: cipher?.type ?? cipherType ?? CipherType.Login, @@ -89,14 +91,26 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ }; } - private async getCipher(id?: CipherId): Promise { + private async getCipher(id: CipherId | null, organization: Organization): Promise { if (id == null) { - return Promise.resolve(null); + return null; } - // Retrieve the cipher through the means of an admin + const localCipher = await this.cipherService.get(id); + + // Fetch from the API because we don't need the permissions in local state OR the cipher was not found (e.g. unassigned) + if (organization.canEditAllCiphers || localCipher == null) { + return await this.getCipherFromAdminApi(id); + } + + return localCipher; + } + + private async getCipherFromAdminApi(id: CipherId): Promise { const cipherResponse = await this.apiService.getCipherAdmin(id); + // Ensure admin response includes permissions that allow editing cipherResponse.edit = true; + cipherResponse.viewPassword = true; const cipherData = new CipherData(cipherResponse); return new Cipher(cipherData); diff --git a/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.component.ts b/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.component.ts index 211d2346230..797fb0e74b8 100644 --- a/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.component.ts @@ -4,6 +4,7 @@ import { firstValueFrom, Subject } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; @@ -42,6 +43,7 @@ export class VaultFilterComponent protected platformUtilsService: PlatformUtilsService, protected billingApiService: BillingApiServiceAbstraction, protected dialogService: DialogService, + protected configService: ConfigService, ) { super( vaultFilterService, @@ -50,6 +52,7 @@ export class VaultFilterComponent platformUtilsService, billingApiService, dialogService, + configService, ); } diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index 9e9264e77cd..400775a9c28 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -10,7 +10,7 @@ {{ freeTrial.message }} hasSubscription !== null)), ]).pipe( - filter(([org, hasSubscription]) => org.isOwner && hasSubscription), + filter( + ([org, hasSubscription]) => org.isOwner && hasSubscription && org.canViewBillingHistory, + ), switchMap(([org]) => combineLatest([ of(org), this.organizationApiService.getSubscription(org.id), - this.organizationApiService.getBilling(org.id), + this.organizationBillingService.getPaymentSource(org.id), ]), ), - map(([org, sub, billing]) => { - return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( - org, - sub, - billing?.paymentSource, - ); + map(([org, sub, paymentSource]) => { + return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(org, sub, paymentSource); }), ); diff --git a/apps/web/src/images/integrations/aws-color.svg b/apps/web/src/images/integrations/aws-color.svg new file mode 100644 index 00000000000..963b65027db --- /dev/null +++ b/apps/web/src/images/integrations/aws-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/aws-darkmode.svg b/apps/web/src/images/integrations/aws-darkmode.svg new file mode 100644 index 00000000000..64c9ba3cf94 --- /dev/null +++ b/apps/web/src/images/integrations/aws-darkmode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/azure-active-directory.svg b/apps/web/src/images/integrations/azure-active-directory.svg new file mode 100644 index 00000000000..22ea64f1f03 --- /dev/null +++ b/apps/web/src/images/integrations/azure-active-directory.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/bitwarden-vertical-blue.svg b/apps/web/src/images/integrations/bitwarden-vertical-blue.svg new file mode 100644 index 00000000000..5d5200364d8 --- /dev/null +++ b/apps/web/src/images/integrations/bitwarden-vertical-blue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/jumpcloud-darkmode.svg b/apps/web/src/images/integrations/jumpcloud-darkmode.svg new file mode 100644 index 00000000000..6969fceeb84 --- /dev/null +++ b/apps/web/src/images/integrations/jumpcloud-darkmode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-auth0-badge-color.svg b/apps/web/src/images/integrations/logo-auth0-badge-color.svg new file mode 100644 index 00000000000..24887cc7510 --- /dev/null +++ b/apps/web/src/images/integrations/logo-auth0-badge-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-duo-color.svg b/apps/web/src/images/integrations/logo-duo-color.svg new file mode 100644 index 00000000000..0959a215708 --- /dev/null +++ b/apps/web/src/images/integrations/logo-duo-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-elastic-badge-color.svg b/apps/web/src/images/integrations/logo-elastic-badge-color.svg new file mode 100644 index 00000000000..f6e00f3d40d --- /dev/null +++ b/apps/web/src/images/integrations/logo-elastic-badge-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-google-badge-color.svg b/apps/web/src/images/integrations/logo-google-badge-color.svg new file mode 100644 index 00000000000..c5a8fe50363 --- /dev/null +++ b/apps/web/src/images/integrations/logo-google-badge-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-jumpcloud-badge-color.svg b/apps/web/src/images/integrations/logo-jumpcloud-badge-color.svg new file mode 100644 index 00000000000..9349186d8a1 --- /dev/null +++ b/apps/web/src/images/integrations/logo-jumpcloud-badge-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-keycloak-icon.svg b/apps/web/src/images/integrations/logo-keycloak-icon.svg new file mode 100644 index 00000000000..862ffcb6c2b --- /dev/null +++ b/apps/web/src/images/integrations/logo-keycloak-icon.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/images/integrations/logo-microsoft-entra-id-color.svg b/apps/web/src/images/integrations/logo-microsoft-entra-id-color.svg new file mode 100644 index 00000000000..a6150c29c62 --- /dev/null +++ b/apps/web/src/images/integrations/logo-microsoft-entra-id-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-microsoft-intune-color.svg b/apps/web/src/images/integrations/logo-microsoft-intune-color.svg new file mode 100644 index 00000000000..2611cf4b3b8 --- /dev/null +++ b/apps/web/src/images/integrations/logo-microsoft-intune-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-microsoft-sentinel-color.svg b/apps/web/src/images/integrations/logo-microsoft-sentinel-color.svg new file mode 100644 index 00000000000..93135526c6f --- /dev/null +++ b/apps/web/src/images/integrations/logo-microsoft-sentinel-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-okta-symbol-black.svg b/apps/web/src/images/integrations/logo-okta-symbol-black.svg new file mode 100644 index 00000000000..876727ad56d --- /dev/null +++ b/apps/web/src/images/integrations/logo-okta-symbol-black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-onelogin-badge-color.svg b/apps/web/src/images/integrations/logo-onelogin-badge-color.svg new file mode 100644 index 00000000000..e2d9ccbc0c1 --- /dev/null +++ b/apps/web/src/images/integrations/logo-onelogin-badge-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-panther-round-color.svg b/apps/web/src/images/integrations/logo-panther-round-color.svg new file mode 100644 index 00000000000..bed05507681 --- /dev/null +++ b/apps/web/src/images/integrations/logo-panther-round-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-ping-identity-badge-color.svg b/apps/web/src/images/integrations/logo-ping-identity-badge-color.svg new file mode 100644 index 00000000000..e34762c249c --- /dev/null +++ b/apps/web/src/images/integrations/logo-ping-identity-badge-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-rapid7-black.svg b/apps/web/src/images/integrations/logo-rapid7-black.svg new file mode 100644 index 00000000000..e2bb7a6f4a8 --- /dev/null +++ b/apps/web/src/images/integrations/logo-rapid7-black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/logo-splunk-black.svg b/apps/web/src/images/integrations/logo-splunk-black.svg new file mode 100644 index 00000000000..d25247bfca8 --- /dev/null +++ b/apps/web/src/images/integrations/logo-splunk-black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/okta-darkmode.svg b/apps/web/src/images/integrations/okta-darkmode.svg new file mode 100644 index 00000000000..e16e0d3c700 --- /dev/null +++ b/apps/web/src/images/integrations/okta-darkmode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/onelogin-darkmode.svg b/apps/web/src/images/integrations/onelogin-darkmode.svg new file mode 100644 index 00000000000..764b1684faa --- /dev/null +++ b/apps/web/src/images/integrations/onelogin-darkmode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/rapid7-darkmode.svg b/apps/web/src/images/integrations/rapid7-darkmode.svg new file mode 100644 index 00000000000..b5f25aae8bd --- /dev/null +++ b/apps/web/src/images/integrations/rapid7-darkmode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/images/integrations/splunk-darkmode.svg b/apps/web/src/images/integrations/splunk-darkmode.svg new file mode 100644 index 00000000000..a4515c0a18c --- /dev/null +++ b/apps/web/src/images/integrations/splunk-darkmode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index eb98a1d7577..163c63a6d24 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -29,6 +29,33 @@ "notifiedMembers": { "message": "Notified members" }, + "revokeMembers": { + "message": "Revoke members" + }, + "restoreMembers": { + "message": "Restore members" + }, + "revokeMembersWarning":{ + "message": "Members with claimed and unclaimed accounts will have different results when revoked:" + }, + "claimedAccountRevoke": { + "message": "Claimed account: Revoke access to Bitwarden account" + }, + "unclaimedAccountRevoke": { + "message": "Unclaimed account: Revoke access to organization data" + }, + "claimedAccount": { + "message": "Claimed account" + }, + "unclaimedAccount": { + "message": "Unclaimed account" + }, + "restoreMembersInstructions": { + "message": "To restore a member's account, go to the Revoked tab. The process may take a few seconds to complete and cannot be interrupted or canceled." + }, + "cannotRestoreAccessError":{ + "message": "Cannot restore organization access" + }, "allApplicationsWithCount": { "message": "All applications ($COUNT$)", "placeholders": { @@ -1110,6 +1137,12 @@ "logInToBitwarden": { "message": "Log in to Bitwarden" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "verifyIdentity": { "message": "Verify your Identity" }, @@ -5610,6 +5643,12 @@ "bulkFilteredMessage": { "message": "Excluded, not applicable for this action" }, + "nonCompliantMembersTitle":{ + "message": "Non-compliant members" + }, + "nonCompliantMembersError":{ + "message": "Members that are non-compliant with the Single organization or Two-step login policy cannot be restored until they adhere to the policy requirements" + }, "fingerprint": { "message": "Fingerprint" }, @@ -6792,6 +6831,10 @@ "message": "Automatically provision users and groups with your preferred identity provider via SCIM provisioning", "description": "the text, 'SCIM', is an acronym and should not be translated." }, + "scimIntegrationDescription": { + "message": "Automatically provision users and groups with your preferred identity provider via SCIM provisioning. Find supported integrations", + "description": "the text, 'SCIM', is an acronym and should not be translated." + }, "scimEnabledCheckboxDesc": { "message": "Enable SCIM", "description": "the text, 'SCIM', is an acronym and should not be translated." @@ -8986,44 +9029,103 @@ "sdksDesc": { "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." }, - "setUpGithubActions": { - "message": "Set up Github Actions" + "singleSignOn": { + "message": "Single sign-on" }, - "setUpKubernetes": { - "message": "Set up Kubernetes" + "ssoDescStart": { + "message": "Configure", + "description": "This represents the beginning of a sentence, broken up to include links. The full sentence will be 'Configure single sign-on for Bitwarden using the implementation guide for your Identity Provider." }, - "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "ssoDescEnd": { + "message": "for Bitwarden using the implementation guide for your Identity Provider.", + "description": "This represents the end of a sentence, broken up to include links. The full sentence will be 'Configure single sign-on for Bitwarden using the implementation guide for your Identity Provider." }, - "setUpAnsible": { - "message": "Set up Ansible" + "userProvisioning":{ + "message": "User provisioning" }, - "rustSDKRepo": { - "message": "View Rust repository" + "scimIntegration": { + "message": "SCIM" }, - "cSharpSDKRepo": { - "message": "View C# repository" + "scimIntegrationDescStart": { + "message": "Configure ", + "description": "This represents the beginning of a sentence, broken up to include links. The full sentence will be 'Configure SCIM (System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider" }, - "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "scimIntegrationDescEnd": { + "message": "(System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider.", + "description": "This represents the end of a sentence, broken up to include links. The full sentence will be 'Configure SCIM (System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider" }, - "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "bwdc":{ + "message": "Bitwarden Directory Connector" }, - "javaSDKRepo": { - "message": "View Java repository" + "bwdcDesc": { + "message": "Configure Bitwarden Directory Connector to automatically provision users and groups using the implementation guide for your Identity Provider." }, - "pythonSDKRepo": { - "message": "View Python repository" + "eventManagement":{ + "message": "Event management" }, - "phpSDKRepo": { - "message": "View php repository" + "eventManagementDesc":{ + "message": "Integrate Bitwarden event logs with your SIEM (system information and event management) system by using the implementation guide for your platform." }, - "rubySDKRepo": { - "message": "View Ruby repository" + "deviceManagement":{ + "message": "Device management" }, - "goSDKRepo": { - "message": "View Go repository" + "deviceManagementDesc":{ + "message": "Configure device management for Bitwarden using the implementation guide for your platform." + + }, + "integrationCardTooltip":{ + "message": "Launch $INTEGRATION$ implementation guide.", + "placeholders": { + "integration": { + "content": "$1", + "example": "Google" + } + } + }, + "smIntegrationTooltip":{ + "message": "Set up $INTEGRATION$.", + "placeholders": { + "integration": { + "content": "$1", + "example": "Google" + } + } + }, + "smSdkTooltip":{ + "message": "View $SDK$ repository", + "placeholders": { + "sdk": { + "content": "$1", + "example": "Rust" + } + } + }, + "integrationCardAriaLabel":{ + "message": "open $INTEGRATION$ implementation guide in a new tab.", + "placeholders": { + "integration": { + "content": "$1", + "example": "google" + } + } + }, + "smSdkAriaLabel":{ + "message": "view $SDK$ repository in a new tab.", + "placeholders": { + "sdk": { + "content": "$1", + "example": "rust" + } + } + }, + "smIntegrationCardAriaLabel":{ + "message": "set up $INTEGRATION$ implementation guide in a new tab.", + "placeholders": { + "integration": { + "content": "$1", + "example": "google" + } + } }, "createNewClientToManageAsProvider": { "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." @@ -9715,9 +9817,19 @@ "description": "Title for the delete organization user dialog" } }, - "deleteOrganizationUserWarning": { - "message": "When a member is deleted, their Bitwarden account and individual vault data will be permanently deleted. Collection data will remain in the organization. To reinstate them they must create an account and be onboarded again.", - "description": "Warning for the delete organization user dialog" + "deleteOrganizationUserWarningDesc": { + "message": "This will permanently delete all items owned by $NAME$. Collection items are not impacted.", + "description": "Warning description for the delete organization user dialog", + "placeholders": { + "name": { + "content": "$1", + "example": "John Doe" + } + } + }, + "deleteManyOrganizationUsersWarningDesc": { + "message": "This will permanently delete all items owned by the following members. Collection items are not impacted.", + "description": "Warning description for the bulk delete organization users dialog" }, "organizationUserDeleted": { "message": "Deleted $NAME$", @@ -9787,5 +9899,8 @@ }, "descriptorCode": { "message": "Descriptor code" + }, + "removeMembers": { + "message": "Remove members" } } diff --git a/apps/web/src/scss/plugins.scss b/apps/web/src/scss/plugins.scss index f4aa428532c..5fbd32ac4ee 100644 --- a/apps/web/src/scss/plugins.scss +++ b/apps/web/src/scss/plugins.scss @@ -12,7 +12,7 @@ } #web-authn-frame { - height: 290px; + height: 315px; @include themify($themes) { background: themed("imgLoading") 0 0 no-repeat; } diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.html b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.html index e30883515e0..7ade2e6c63d 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.html @@ -1,6 +1,11 @@ -

{{ "scimDescription" | i18n }}

+

+ {{ "scimIntegrationDescription" | i18n }} + +

): Promise { - const singleOrgEnabled = policiesEnabledMap.get(PolicyType.SingleOrg) ?? false; - if (this.enabled.value && !singleOrgEnabled) { - throw new Error(this.i18nService.t("requireSsoPolicyReqError")); - } - - const data = this.buildRequestData(); - if (data?.minutes == null || data?.minutes <= 0) { + async buildRequest(): Promise { + const request = await super.buildRequest(); + if (request.data?.minutes == null || request.data?.minutes <= 0) { throw new Error(this.i18nService.t("invalidMaximumVaultTimeout")); } - return super.buildRequest(policiesEnabledMap); + return request; } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts index a4461b3e11a..68264593b8e 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts @@ -1,4 +1,5 @@ -import { Component, OnInit } from "@angular/core"; +import { Component, OnInit, SecurityContext } from "@angular/core"; +import { DomSanitizer } from "@angular/platform-browser"; import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; @@ -24,6 +25,7 @@ export class VerifyRecoverDeleteProviderComponent implements OnInit { private i18nService: I18nService, private route: ActivatedRoute, private toastService: ToastService, + private sanitizer: DomSanitizer, ) {} async ngOnInit() { @@ -31,7 +33,10 @@ export class VerifyRecoverDeleteProviderComponent implements OnInit { if (qParams.providerId != null && qParams.token != null && qParams.name != null) { this.providerId = qParams.providerId; this.token = qParams.token; - this.name = qParams.name; + this.name = + qParams.name && typeof qParams.name === "string" + ? this.sanitizer.sanitize(SecurityContext.HTML, qParams.name) || "" + : ""; } else { await this.router.navigate(["/"]); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.ts deleted file mode 100644 index 058d59d702c..00000000000 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component, Input } from "@angular/core"; - -import { IntegrationType } from "@bitwarden/common/enums"; - -import { Integration } from "../models/integration"; - -@Component({ - selector: "sm-integration-grid", - templateUrl: "./integration-grid.component.html", -}) -export class IntegrationGridComponent { - @Input() integrations: Integration[]; - - protected IntegrationType = IntegrationType; -} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.html index a2f21888613..db3db75897d 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.html @@ -4,7 +4,11 @@

{{ "integrationsDesc" | i18n }}

- +
@@ -12,5 +16,9 @@ {{ "sdks" | i18n }}

{{ "sdksDesc" | i18n }}

- + diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts index 6c8ea28bc2f..e4a65f7ddd8 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts @@ -4,14 +4,17 @@ import { By } from "@angular/platform-browser"; import { mock } from "jest-mock-extended"; import { of } from "rxjs"; +import { SharedModule } from "@bitwarden/components/src/shared"; +import { + IntegrationCardComponent, + IntegrationGridComponent, +} from "@bitwarden/web-vault/app/shared"; + import { SYSTEM_THEME_OBSERVABLE } from "../../../../../../libs/angular/src/services/injection-tokens"; import { I18nService } from "../../../../../../libs/common/src/platform/abstractions/i18n.service"; import { ThemeType } from "../../../../../../libs/common/src/platform/enums"; import { ThemeStateService } from "../../../../../../libs/common/src/platform/theming/theme-state.service"; -import { I18nPipe } from "../../../../../../libs/components/src/shared/i18n.pipe"; -import { IntegrationCardComponent } from "./integration-card/integration-card.component"; -import { IntegrationGridComponent } from "./integration-grid/integration-grid.component"; import { IntegrationsComponent } from "./integrations.component"; @Component({ @@ -31,18 +34,12 @@ describe("IntegrationsComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ - IntegrationsComponent, - IntegrationGridComponent, - IntegrationCardComponent, - MockHeaderComponent, - MockNewMenuComponent, - I18nPipe, - ], + declarations: [IntegrationsComponent, MockHeaderComponent, MockNewMenuComponent], + imports: [IntegrationGridComponent, IntegrationCardComponent, SharedModule], providers: [ { provide: I18nService, - useValue: mock({ t: (key) => key }), + useValue: mock(), }, { provide: ThemeStateService, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts index 9e846d45034..b8f9386d715 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts @@ -1,9 +1,7 @@ import { Component } from "@angular/core"; import { IntegrationType } from "@bitwarden/common/enums"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; - -import { Integration } from "./models/integration"; +import { Integration } from "@bitwarden/web-vault/app/shared"; @Component({ selector: "sm-integrations", @@ -12,11 +10,10 @@ import { Integration } from "./models/integration"; export class IntegrationsComponent { private integrationsAndSdks: Integration[] = []; - constructor(i18nService: I18nService) { + constructor() { this.integrationsAndSdks = [ { name: "Rust", - linkText: i18nService.t("rustSDKRepo"), linkURL: "https://github.com/bitwarden/sdk", image: "../../../../../../../images/secrets-manager/sdks/rust.svg", imageDarkMode: "../../../../../../../images/secrets-manager/sdks/rust-white.svg", @@ -24,7 +21,6 @@ export class IntegrationsComponent { }, { name: "GitHub Actions", - linkText: i18nService.t("setUpGithubActions"), linkURL: "https://bitwarden.com/help/github-actions-integration/", image: "../../../../../../../images/secrets-manager/integrations/github.svg", imageDarkMode: "../../../../../../../images/secrets-manager/integrations/github-white.svg", @@ -32,7 +28,6 @@ export class IntegrationsComponent { }, { name: "GitLab CI/CD", - linkText: i18nService.t("setUpGitlabCICD"), linkURL: "https://bitwarden.com/help/gitlab-integration/", image: "../../../../../../../images/secrets-manager/integrations/gitlab.svg", imageDarkMode: "../../../../../../../images/secrets-manager/integrations/gitlab-white.svg", @@ -40,35 +35,30 @@ export class IntegrationsComponent { }, { name: "Ansible", - linkText: i18nService.t("setUpAnsible"), linkURL: "https://bitwarden.com/help/ansible-integration/", image: "../../../../../../../images/secrets-manager/integrations/ansible.svg", type: IntegrationType.Integration, }, { name: "C#", - linkText: i18nService.t("cSharpSDKRepo"), linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/csharp", image: "../../../../../../../images/secrets-manager/sdks/c-sharp.svg", type: IntegrationType.SDK, }, { name: "C++", - linkText: i18nService.t("cPlusPlusSDKRepo"), linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/cpp", image: "../../../../../../../images/secrets-manager/sdks/c-plus-plus.png", type: IntegrationType.SDK, }, { name: "Go", - linkText: i18nService.t("goSDKRepo"), linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/go", image: "../../../../../../../images/secrets-manager/sdks/go.svg", type: IntegrationType.SDK, }, { name: "Java", - linkText: i18nService.t("javaSDKRepo"), linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/java", image: "../../../../../../../images/secrets-manager/sdks/java.svg", imageDarkMode: "../../../../../../../images/secrets-manager/sdks/java-white.svg", @@ -76,35 +66,30 @@ export class IntegrationsComponent { }, { name: "JS WebAssembly", - linkText: i18nService.t("jsWebAssemblySDKRepo"), linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/js", image: "../../../../../../../images/secrets-manager/sdks/wasm.svg", type: IntegrationType.SDK, }, { name: "php", - linkText: i18nService.t("phpSDKRepo"), linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/php", image: "../../../../../../../images/secrets-manager/sdks/php.svg", type: IntegrationType.SDK, }, { name: "Python", - linkText: i18nService.t("pythonSDKRepo"), linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/python", image: "../../../../../../../images/secrets-manager/sdks/python.svg", type: IntegrationType.SDK, }, { name: "Ruby", - linkText: i18nService.t("rubySDKRepo"), linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/ruby", image: "../../../../../../../images/secrets-manager/sdks/ruby.png", type: IntegrationType.SDK, }, { name: "Kubernetes Operator", - linkText: i18nService.t("setUpKubernetes"), linkURL: "https://bitwarden.com/help/secrets-manager-kubernetes-operator/", image: "../../../../../../../images/secrets-manager/integrations/kubernetes.svg", type: IntegrationType.Integration, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts index 0d26b626f16..b79892f5ed6 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts @@ -1,15 +1,22 @@ import { NgModule } from "@angular/core"; +import { + IntegrationCardComponent, + IntegrationGridComponent, +} from "@bitwarden/web-vault/app/shared"; + import { SecretsManagerSharedModule } from "../shared/sm-shared.module"; -import { IntegrationCardComponent } from "./integration-card/integration-card.component"; -import { IntegrationGridComponent } from "./integration-grid/integration-grid.component"; import { IntegrationsRoutingModule } from "./integrations-routing.module"; import { IntegrationsComponent } from "./integrations.component"; @NgModule({ - imports: [SecretsManagerSharedModule, IntegrationsRoutingModule], - declarations: [IntegrationsComponent, IntegrationGridComponent, IntegrationCardComponent], - providers: [], + imports: [ + SecretsManagerSharedModule, + IntegrationsRoutingModule, + IntegrationCardComponent, + IntegrationGridComponent, + ], + declarations: [IntegrationsComponent], }) export class IntegrationsModule {} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.html index 31746e7601c..482ccaf0454 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.html @@ -10,7 +10,7 @@ {{ freeTrial.message }} org.isOwner), + filter((org) => org.isOwner && org.canViewBillingHistory && org.canViewSubscription), switchMap((org) => combineLatest([ of(org), this.organizationApiService.getSubscription(org.id), - this.organizationApiService.getBilling(org.id), + this.organizationBillingService.getPaymentSource(org.id), ]), ), - map(([org, sub, billing]) => { - return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( - org, - sub, - billing?.paymentSource, - ); + map(([org, sub, paymentSource]) => { + return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(org, sub, paymentSource); }), takeUntil(this.destroy$), ); diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts index c13cc0efae8..993d9c0a134 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts @@ -1,15 +1,14 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; -import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { organizationPermissionsGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/org-permissions.guard"; import { RiskInsightsComponent } from "./risk-insights.component"; const routes: Routes = [ { path: "risk-insights", - canActivate: [canAccessFeature(FeatureFlag.AccessIntelligence)], + canActivate: [organizationPermissionsGuard((org) => org.useRiskInsights)], component: RiskInsightsComponent, data: { titleId: "RiskInsights", diff --git a/bitwarden_license/bit-web/tsconfig.json b/bitwarden_license/bit-web/tsconfig.json index 3ccdade273e..09de92d355d 100644 --- a/bitwarden_license/bit-web/tsconfig.json +++ b/bitwarden_license/bit-web/tsconfig.json @@ -46,6 +46,7 @@ "../../apps/web/src/**/*.spec.ts", "../../libs/common/src/platform/services/**/*.worker.ts", - "src/**/*.stories.ts" + "src/**/*.stories.ts", + "src/**/*.spec.ts" ] } diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-expired.component.ts b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-expired.component.ts new file mode 100644 index 00000000000..faa08cf073b --- /dev/null +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-expired.component.ts @@ -0,0 +1,25 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { RouterModule } from "@angular/router"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ButtonModule } from "@bitwarden/components"; + +/** + * This component is used to display a message to the user that their authentication session has expired. + * It provides a button to navigate to the login page. + */ +@Component({ + selector: "app-two-factor-expired", + standalone: true, + imports: [CommonModule, JslibModule, ButtonModule, RouterModule], + template: ` +

+ {{ "authenticationSessionTimedOut" | i18n }} +

+
+ {{ "logIn" | i18n }} + + `, +}) +export class TwoFactorTimeoutComponent {} diff --git a/libs/angular/src/auth/components/two-factor.component.spec.ts b/libs/angular/src/auth/components/two-factor.component.spec.ts index e21d119adf8..5a1903d6671 100644 --- a/libs/angular/src/auth/components/two-factor.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor.component.spec.ts @@ -86,9 +86,12 @@ describe("TwoFactorComponent", () => { }; let selectedUserDecryptionOptions: BehaviorSubject; + let twoFactorTimeoutSubject: BehaviorSubject; beforeEach(() => { + twoFactorTimeoutSubject = new BehaviorSubject(false); mockLoginStrategyService = mock(); + mockLoginStrategyService.twoFactorTimeout$ = twoFactorTimeoutSubject; mockRouter = mock(); mockI18nService = mock(); mockApiService = mock(); @@ -492,4 +495,10 @@ describe("TwoFactorComponent", () => { }); }); }); + + it("navigates to the timeout route when timeout expires", async () => { + twoFactorTimeoutSubject.next(true); + + expect(mockRouter.navigate).toHaveBeenCalledWith(["2fa-timeout"]); + }); }); diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index eaff9d665fd..33269e28e96 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -1,4 +1,5 @@ -import { Directive, Inject, OnDestroy, OnInit } from "@angular/core"; +import { Directive, Inject, OnInit, OnDestroy } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, NavigationExtras, Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; import { first } from "rxjs/operators"; @@ -68,6 +69,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected changePasswordRoute = "set-password"; protected forcePasswordResetRoute = "update-temp-password"; protected successRoute = "vault"; + protected twoFactorTimeoutRoute = "2fa-timeout"; get isDuoProvider(): boolean { return ( @@ -99,6 +101,21 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI ) { super(environmentService, i18nService, platformUtilsService, toastService); this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); + + // Add subscription to twoFactorTimeout$ and navigate to twoFactorTimeoutRoute if expired + this.loginStrategyService.twoFactorTimeout$ + .pipe(takeUntilDestroyed()) + .subscribe(async (expired) => { + if (!expired) { + return; + } + + try { + await this.router.navigate([this.twoFactorTimeoutRoute]); + } catch (err) { + this.logService.error(`Failed to navigate to ${this.twoFactorTimeoutRoute} route`, err); + } + }); } async ngOnInit() { diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 0208a3cdc7a..a43f1fa07a8 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1213,6 +1213,8 @@ const safeProviders: SafeProvider[] = [ useClass: OrganizationBillingService, deps: [ ApiServiceAbstraction, + BillingApiServiceAbstraction, + ConfigService, KeyServiceAbstraction, EncryptService, I18nServiceAbstraction, diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 3747236d51d..1bc58368d8f 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -699,7 +699,6 @@ export class AddEditComponent implements OnInit, OnDestroy { } protected saveCipher(cipher: Cipher) { - const isNotClone = this.editMode && !this.cloneMode; let orgAdmin = this.organization?.canEditAllCiphers; // if a cipher is unassigned we want to check if they are an admin or have permission to edit any collection @@ -709,7 +708,7 @@ export class AddEditComponent implements OnInit, OnDestroy { return this.cipher.id == null ? this.cipherService.createWithServer(cipher, orgAdmin) - : this.cipherService.updateWithServer(cipher, orgAdmin, isNotClone); + : this.cipherService.updateWithServer(cipher, orgAdmin); } protected deleteCipher() { diff --git a/libs/auth/src/angular/icons/index.ts b/libs/auth/src/angular/icons/index.ts index 9c444df5702..05bb630fcb3 100644 --- a/libs/auth/src/angular/icons/index.ts +++ b/libs/auth/src/angular/icons/index.ts @@ -10,3 +10,4 @@ export * from "./vault.icon"; export * from "./registration-user-add.icon"; export * from "./registration-lock-alt.icon"; export * from "./registration-expired-link.icon"; +export * from "./two-factor-timeout.icon"; diff --git a/libs/auth/src/angular/icons/two-factor-timeout.icon.ts b/libs/auth/src/angular/icons/two-factor-timeout.icon.ts new file mode 100644 index 00000000000..71d0aa549dc --- /dev/null +++ b/libs/auth/src/angular/icons/two-factor-timeout.icon.ts @@ -0,0 +1,8 @@ +import { svgIcon } from "@bitwarden/components"; + +export const TwoFactorTimeoutIcon = svgIcon` + + + + +`; diff --git a/libs/auth/src/common/abstractions/login-strategy.service.ts b/libs/auth/src/common/abstractions/login-strategy.service.ts index a46636532bf..e86cd6b0b0d 100644 --- a/libs/auth/src/common/abstractions/login-strategy.service.ts +++ b/libs/auth/src/common/abstractions/login-strategy.service.ts @@ -71,4 +71,8 @@ export abstract class LoginStrategyServiceAbstraction { * Creates a master key from the provided master password and email. */ makePreloginKey: (masterPassword: string, email: string) => Promise; + /** + * Emits true if the two factor session has expired. + */ + twoFactorTimeout$: Observable; } diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index b86d1e3f3b4..99e3c057e11 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -6,6 +6,7 @@ import { Observable, shareReplay, Subscription, + BehaviorSubject, } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -68,7 +69,7 @@ import { CACHE_KEY, } from "./login-strategy.state"; -const sessionTimeoutLength = 2 * 60 * 1000; // 2 minutes +const sessionTimeoutLength = 5 * 60 * 1000; // 5 minutes export class LoginStrategyService implements LoginStrategyServiceAbstraction { private sessionTimeoutSubscription: Subscription; @@ -76,6 +77,9 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { private loginStrategyCacheState: GlobalState; private loginStrategyCacheExpirationState: GlobalState; private authRequestPushNotificationState: GlobalState; + private twoFactorTimeoutSubject = new BehaviorSubject(false); + + twoFactorTimeout$: Observable = this.twoFactorTimeoutSubject.asObservable(); private loginStrategy$: Observable< | UserApiLoginStrategy @@ -123,7 +127,14 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { ); this.taskSchedulerService.registerTaskHandler( ScheduledTaskNames.loginStrategySessionTimeout, - () => this.clearCache(), + async () => { + this.twoFactorTimeoutSubject.next(true); + try { + await this.clearCache(); + } catch (e) { + this.logService.error("Failed to clear cache during session timeout", e); + } + }, ); this.currentAuthType$ = this.currentAuthnTypeState.state$; @@ -189,6 +200,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { | WebAuthnLoginCredentials, ): Promise { await this.clearCache(); + this.twoFactorTimeoutSubject.next(false); await this.currentAuthnTypeState.update((_) => credentials.type); @@ -273,6 +285,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { private async clearCache(): Promise { await this.currentAuthnTypeState.update((_) => null); await this.loginStrategyCacheState.update((_) => null); + this.twoFactorTimeoutSubject.next(false); await this.clearSessionTimeout(); } diff --git a/libs/common/src/admin-console/models/data/organization.data.spec.ts b/libs/common/src/admin-console/models/data/organization.data.spec.ts index 12c5339a245..da9a82e7c5c 100644 --- a/libs/common/src/admin-console/models/data/organization.data.spec.ts +++ b/libs/common/src/admin-console/models/data/organization.data.spec.ts @@ -56,6 +56,7 @@ describe("ORGANIZATIONS state", () => { allowAdminAccessToAllCollectionItems: false, familySponsorshipLastSyncDate: new Date(), userIsManagedByOrganization: false, + useRiskInsights: false, }, }; const result = sut.deserializer(JSON.parse(JSON.stringify(expectedResult))); diff --git a/libs/common/src/admin-console/models/data/organization.data.ts b/libs/common/src/admin-console/models/data/organization.data.ts index d8032fd6fcf..23a0f360f9e 100644 --- a/libs/common/src/admin-console/models/data/organization.data.ts +++ b/libs/common/src/admin-console/models/data/organization.data.ts @@ -56,6 +56,7 @@ export class OrganizationData { limitCollectionDeletion: boolean; allowAdminAccessToAllCollectionItems: boolean; userIsManagedByOrganization: boolean; + useRiskInsights: boolean; constructor( response?: ProfileOrganizationResponse, @@ -116,6 +117,7 @@ export class OrganizationData { this.limitCollectionDeletion = response.limitCollectionDeletion; this.allowAdminAccessToAllCollectionItems = response.allowAdminAccessToAllCollectionItems; this.userIsManagedByOrganization = response.userIsManagedByOrganization; + this.useRiskInsights = response.useRiskInsights; this.isMember = options.isMember; this.isProviderUser = options.isProviderUser; diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index 497d3b0889b..3f3ec0f4457 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -81,6 +81,7 @@ export class Organization { * matches one of the verified domains of that organization, and the user is a member of it. */ userIsManagedByOrganization: boolean; + useRiskInsights: boolean; constructor(obj?: OrganizationData) { if (obj == null) { @@ -137,6 +138,7 @@ export class Organization { this.limitCollectionDeletion = obj.limitCollectionDeletion; this.allowAdminAccessToAllCollectionItems = obj.allowAdminAccessToAllCollectionItems; this.userIsManagedByOrganization = obj.userIsManagedByOrganization; + this.useRiskInsights = obj.useRiskInsights; } get canAccess() { diff --git a/libs/common/src/admin-console/models/response/organization.response.ts b/libs/common/src/admin-console/models/response/organization.response.ts index e033646797d..fd460896a31 100644 --- a/libs/common/src/admin-console/models/response/organization.response.ts +++ b/libs/common/src/admin-console/models/response/organization.response.ts @@ -35,6 +35,7 @@ export class OrganizationResponse extends BaseResponse { limitCollectionCreation: boolean; limitCollectionDeletion: boolean; allowAdminAccessToAllCollectionItems: boolean; + useRiskInsights: boolean; constructor(response: any) { super(response); @@ -75,5 +76,6 @@ export class OrganizationResponse extends BaseResponse { this.allowAdminAccessToAllCollectionItems = this.getResponseProperty( "AllowAdminAccessToAllCollectionItems", ); + this.useRiskInsights = this.getResponseProperty("UseRiskInsights"); } } diff --git a/libs/common/src/admin-console/models/response/profile-organization.response.ts b/libs/common/src/admin-console/models/response/profile-organization.response.ts index ed1f45d4917..9c4b8885ab8 100644 --- a/libs/common/src/admin-console/models/response/profile-organization.response.ts +++ b/libs/common/src/admin-console/models/response/profile-organization.response.ts @@ -53,6 +53,7 @@ export class ProfileOrganizationResponse extends BaseResponse { limitCollectionDeletion: boolean; allowAdminAccessToAllCollectionItems: boolean; userIsManagedByOrganization: boolean; + useRiskInsights: boolean; constructor(response: any) { super(response); @@ -117,5 +118,6 @@ export class ProfileOrganizationResponse extends BaseResponse { "AllowAdminAccessToAllCollectionItems", ); this.userIsManagedByOrganization = this.getResponseProperty("UserIsManagedByOrganization"); + this.useRiskInsights = this.getResponseProperty("UseRiskInsights"); } } diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index 72902baa30e..0bc1f3bc558 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -1,3 +1,6 @@ +import { BillingSourceResponse } from "@bitwarden/common/billing/models/response/billing.response"; +import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; + import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; import { InitiationPath } from "../../models/request/reference-event.request"; import { PaymentMethodType, PlanType } from "../enums"; @@ -41,11 +44,15 @@ export type SubscriptionInformation = { }; export abstract class OrganizationBillingServiceAbstraction { - purchaseSubscription: (subscription: SubscriptionInformation) => Promise; + getPaymentSource: ( + organizationId: string, + ) => Promise; - startFree: (subscription: SubscriptionInformation) => Promise; + purchaseSubscription: (subscription: SubscriptionInformation) => Promise; purchaseSubscriptionNoPaymentMethod: ( subscription: SubscriptionInformation, ) => Promise; + + startFree: (subscription: SubscriptionInformation) => Promise; } diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index efc36278532..487098620bd 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -1,4 +1,18 @@ -import { KeyService } from "../../../../key-management/src/abstractions/key.service"; +import { + BillingApiServiceAbstraction, + OrganizationBillingServiceAbstraction, + OrganizationInformation, + PaymentInformation, + PlanInformation, + SubscriptionInformation, +} from "@bitwarden/common/billing/abstractions"; +import { BillingSourceResponse } from "@bitwarden/common/billing/models/response/billing.response"; +import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; +import { KeyService } from "@bitwarden/key-management"; + import { ApiService } from "../../abstractions/api.service"; import { OrganizationApiServiceAbstraction as OrganizationApiService } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; @@ -8,14 +22,6 @@ import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { EncString } from "../../platform/models/domain/enc-string"; import { OrgKey } from "../../types/key"; -import { SyncService } from "../../vault/abstractions/sync/sync.service.abstraction"; -import { - OrganizationBillingServiceAbstraction, - OrganizationInformation, - PaymentInformation, - PlanInformation, - SubscriptionInformation, -} from "../abstractions/organization-billing.service"; import { PlanType } from "../enums"; import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request"; @@ -29,6 +35,8 @@ interface OrganizationKeys { export class OrganizationBillingService implements OrganizationBillingServiceAbstraction { constructor( private apiService: ApiService, + private billingApiService: BillingApiServiceAbstraction, + private configService: ConfigService, private keyService: KeyService, private encryptService: EncryptService, private i18nService: I18nService, @@ -36,6 +44,23 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs private syncService: SyncService, ) {} + async getPaymentSource( + organizationId: string, + ): Promise { + const deprecateStripeSourcesAPI = await this.configService.getFeatureFlag( + FeatureFlag.AC2476_DeprecateStripeSourcesAPI, + ); + + if (deprecateStripeSourcesAPI) { + const paymentMethod = + await this.billingApiService.getOrganizationPaymentMethod(organizationId); + return paymentMethod.paymentSource; + } else { + const billing = await this.organizationApiService.getBilling(organizationId); + return billing.paymentSource; + } + } + async purchaseSubscription(subscription: SubscriptionInformation): Promise { const request = new OrganizationCreateRequest(); @@ -58,26 +83,6 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs return response; } - async startFree(subscription: SubscriptionInformation): Promise { - const request = new OrganizationCreateRequest(); - - const organizationKeys = await this.makeOrganizationKeys(); - - this.setOrganizationKeys(request, organizationKeys); - - this.setOrganizationInformation(request, subscription.organization); - - this.setPlanInformation(request, subscription.plan); - - const response = await this.organizationApiService.create(request); - - await this.apiService.refreshIdentityToken(); - - await this.syncService.fullSync(true); - - return response; - } - async purchaseSubscriptionNoPaymentMethod( subscription: SubscriptionInformation, ): Promise { @@ -100,6 +105,26 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs return response; } + async startFree(subscription: SubscriptionInformation): Promise { + const request = new OrganizationCreateRequest(); + + const organizationKeys = await this.makeOrganizationKeys(); + + this.setOrganizationKeys(request, organizationKeys); + + this.setOrganizationInformation(request, subscription.organization); + + this.setPlanInformation(request, subscription.plan); + + const response = await this.organizationApiService.create(request); + + await this.apiService.refreshIdentityToken(); + + await this.syncService.fullSync(true); + + return response; + } + private async makeOrganizationKeys(): Promise { const [encryptedKey, key] = await this.keyService.makeOrgKey(); const [publicKey, encryptedPrivateKey] = await this.keyService.makeKeyPair(key); diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 6b8af3ef78d..f79ebf8aa55 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -31,14 +31,14 @@ export enum FeatureFlag { CipherKeyEncryption = "cipher-key-encryption", VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint", PM11901_RefactorSelfHostingLicenseUploader = "PM-11901-refactor-self-hosting-license-uploader", - AccessIntelligence = "pm-13227-access-intelligence", - Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions", + PM14505AdminConsoleIntegrationPage = "pm-14505-admin-console-integration-page", CriticalApps = "pm-14466-risk-insights-critical-application", TrialPaymentOptional = "PM-8163-trial-payment", SecurityTasks = "security-tasks", NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss", NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss", DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship", + MacOsNativeCredentialSync = "macos-native-credential-sync", PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission", } @@ -81,14 +81,14 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.CipherKeyEncryption]: FALSE, [FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE, [FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader]: FALSE, - [FeatureFlag.AccessIntelligence]: FALSE, - [FeatureFlag.Pm13322AddPolicyDefinitions]: FALSE, + [FeatureFlag.PM14505AdminConsoleIntegrationPage]: FALSE, [FeatureFlag.CriticalApps]: FALSE, [FeatureFlag.TrialPaymentOptional]: FALSE, [FeatureFlag.SecurityTasks]: FALSE, [FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE, [FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE, [FeatureFlag.DisableFreeFamiliesSponsorship]: FALSE, + [FeatureFlag.MacOsNativeCredentialSync]: FALSE, [FeatureFlag.PM11360RemoveProviderExportPermission]: FALSE, } satisfies Record; diff --git a/libs/common/src/enums/integration-type.enum.ts b/libs/common/src/enums/integration-type.enum.ts index acb95106976..42c385fe715 100644 --- a/libs/common/src/enums/integration-type.enum.ts +++ b/libs/common/src/enums/integration-type.enum.ts @@ -1,4 +1,9 @@ export enum IntegrationType { Integration = "integration", SDK = "sdk", + SSO = "sso", + SCIM = "scim", + BWDC = "bwdc", + EVENT = "event", + DEVICE = "device", } diff --git a/libs/common/src/platform/services/fido2/fido2-autofill-utils.ts b/libs/common/src/platform/services/fido2/fido2-autofill-utils.ts new file mode 100644 index 00000000000..7ef705b95f9 --- /dev/null +++ b/libs/common/src/platform/services/fido2/fido2-autofill-utils.ts @@ -0,0 +1,26 @@ +// TODO: Add tests for this method + +import { CipherType } from "../../../vault/enums"; +import { CipherView } from "../../../vault/models/view/cipher.view"; +import { Fido2CredentialAutofillView } from "../../../vault/models/view/fido2-credential-autofill.view"; + +// TODO: Move into Fido2AuthenticatorService +export async function getCredentialsForAutofill( + ciphers: CipherView[], +): Promise { + return ciphers + .filter( + (cipher) => + !cipher.isDeleted && cipher.type === CipherType.Login && cipher.login.hasFido2Credentials, + ) + .map((cipher) => { + const credential = cipher.login.fido2Credentials[0]; + return { + cipherId: cipher.id, + credentialId: credential.credentialId, + rpId: credential.rpId, + userHandle: credential.userHandle, + userName: credential.userName, + }; + }); +} diff --git a/libs/common/src/vault/models/view/fido2-credential-autofill.view.ts b/libs/common/src/vault/models/view/fido2-credential-autofill.view.ts new file mode 100644 index 00000000000..b120f27b774 --- /dev/null +++ b/libs/common/src/vault/models/view/fido2-credential-autofill.view.ts @@ -0,0 +1,7 @@ +export class Fido2CredentialAutofillView { + cipherId: string; + credentialId: string; + rpId: string; + userHandle: string; + userName: string; +} diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 961bc03bbb0..6b225af0d84 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -229,11 +229,11 @@ describe("Cipher Service", () => { }); describe("updateWithServer()", () => { - it("should call apiService.putCipherAdmin when orgAdmin and isNotClone params are true", async () => { + it("should call apiService.putCipherAdmin when orgAdmin param is true", async () => { const spy = jest .spyOn(apiService, "putCipherAdmin") .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); - await cipherService.updateWithServer(cipherObj, true, true); + await cipherService.updateWithServer(cipherObj, true); const expectedObj = new CipherRequest(cipherObj); expect(spy).toHaveBeenCalled(); @@ -252,7 +252,7 @@ describe("Cipher Service", () => { expect(spy).toHaveBeenCalledWith(cipherObj.id, expectedObj); }); - it("should call apiService.putPartialCipher when orgAdmin, isNotClone, and edit are false", async () => { + it("should call apiService.putPartialCipher when orgAdmin, and edit are false", async () => { cipherObj.edit = false; const spy = jest .spyOn(apiService, "putPartialCipher") diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 474976932e8..19a6e63e5b5 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -709,13 +709,9 @@ export class CipherService implements CipherServiceAbstraction { return new Cipher(updated[cipher.id as CipherId]); } - async updateWithServer( - cipher: Cipher, - orgAdmin?: boolean, - isNotClone?: boolean, - ): Promise { + async updateWithServer(cipher: Cipher, orgAdmin?: boolean): Promise { let response: CipherResponse; - if (orgAdmin && isNotClone) { + if (orgAdmin) { const request = new CipherRequest(cipher); response = await this.apiService.putCipherAdmin(cipher.id, request); const data = new CipherData(response, cipher.collectionIds); diff --git a/libs/node/src/services/node-crypto-function.service.ts b/libs/node/src/services/node-crypto-function.service.ts index c8d676eeaa5..b4126b0813f 100644 --- a/libs/node/src/services/node-crypto-function.service.ts +++ b/libs/node/src/services/node-crypto-function.service.ts @@ -1,6 +1,5 @@ import * as crypto from "crypto"; -import * as argon2 from "argon2"; import * as forge from "node-forge"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -40,6 +39,7 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { const nodePassword = this.toNodeValue(password); const nodeSalt = this.toNodeBuffer(this.toUint8Buffer(salt)); + const argon2 = await import("argon2"); const hash = await argon2.hash(nodePassword, { salt: nodeSalt, raw: true, diff --git a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html index 75334b68ef9..344348f6a90 100644 --- a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html +++ b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html @@ -3,11 +3,19 @@ {{ (hideIcon ? "createSend" : "new") | i18n }} - + {{ "sendTypeText" | i18n }} - + {{ "sendTypeFile" | i18n }}