diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index 3b1f7d3605f..5039fe57a30 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -49,6 +49,7 @@ import type { NativeWindowObject } from "./desktop-fido2-user-interface.service" @Injectable() export class DesktopAutofillService implements OnDestroy { private destroy$ = new Subject(); + private registrationRequest: autofill.PasskeyRegistrationRequest; constructor( private logService: LogService, @@ -184,8 +185,14 @@ export class DesktopAutofillService implements OnDestroy { }); } + get lastRegistrationRequest() { + return this.registrationRequest; + } + listenIpc() { ipc.autofill.listenPasskeyRegistration(async (clientId, sequenceNumber, request, callback) => { + this.registrationRequest = request; + this.logService.warning("listenPasskeyRegistration", clientId, sequenceNumber, request); this.logService.warning( "listenPasskeyRegistration2", diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 168753a1f2c..f6b9a5baf79 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3574,9 +3574,18 @@ "saveNewPasskey": { "message": "Save as new login" }, + "overwritePasskey": { + "message": "Overwrite passkey?" + }, "unableToSavePasskey": { "message": "Unable to save passkey" }, + "alreadyContainsPasskey": { + "message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?" + }, + "passkeyAlreadyExists": { + "message": "A passkey already exists for this application." + }, "closeBitwarden": { "message": "Close Bitwarden" }, diff --git a/apps/desktop/src/modal/passkeys/create/fido2-create.component.ts b/apps/desktop/src/modal/passkeys/create/fido2-create.component.ts index 8e2175ece03..7aeae6dce68 100644 --- a/apps/desktop/src/modal/passkeys/create/fido2-create.component.ts +++ b/apps/desktop/src/modal/passkeys/create/fido2-create.component.ts @@ -8,6 +8,10 @@ import { BitwardenShield } from "@bitwarden/auth/angular"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { + compareCredentialIds, + parseCredentialId, +} from "@bitwarden/common/platform/services/fido2/credential-id-utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { @@ -24,6 +28,7 @@ import { } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; +import { DesktopAutofillService } from "../../../autofill/services/desktop-autofill.service"; import { DesktopFido2UserInterfaceService, DesktopFido2UserInterfaceSession, @@ -52,6 +57,7 @@ export class Fido2CreateComponent implements OnInit, OnDestroy { session?: DesktopFido2UserInterfaceSession = null; private ciphersSubject = new BehaviorSubject([]); ciphers$: Observable = this.ciphersSubject.asObservable(); + containsExcludedCiphers: boolean = false; readonly Icons = { BitwardenShield }; constructor( @@ -59,6 +65,7 @@ export class Fido2CreateComponent implements OnInit, OnDestroy { private readonly fido2UserInterfaceService: DesktopFido2UserInterfaceService, private readonly accountService: AccountService, private readonly cipherService: CipherService, + private readonly desktopAutofillService: DesktopAutofillService, private readonly dialogService: DialogService, private readonly domainSettingsService: DomainSettingsService, private readonly logService: LogService, @@ -69,6 +76,7 @@ export class Fido2CreateComponent implements OnInit, OnDestroy { async ngOnInit() { await this.accountService.setShowHeader(false); this.session = this.fido2UserInterfaceService.getCurrentSession(); + const lastRegistrationRequest = this.desktopAutofillService.lastRegistrationRequest; const rpid = await this.session.getRpId(); const equivalentDomains = await firstValueFrom( this.domainSettingsService.getUrlEquivalentDomains(rpid), @@ -80,16 +88,42 @@ export class Fido2CreateComponent implements OnInit, OnDestroy { this.cipherService .getAllDecrypted(activeUserId) .then((ciphers) => { - const relevantCiphers = ciphers.filter((cipher) => { - if (!cipher.login || !cipher.login.hasUris) { - return false; - } + if (lastRegistrationRequest.excludedCredentials.length > 0) { + const excludedCiphers = ciphers.filter((cipher) => { + const credentialId = cipher.login.hasFido2Credentials + ? parseCredentialId(cipher.login.fido2Credentials[0]?.credentialId) + : new Uint8Array(); + if (!cipher.login || !cipher.login.hasUris) { + return false; + } - return ( - cipher.login.matchesUri(rpid, equivalentDomains) && !cipher.login.hasFido2Credentials - ); - }); - this.ciphersSubject.next(relevantCiphers); + return ( + cipher.login.matchesUri(rpid, equivalentDomains) && + compareCredentialIds( + credentialId, + new Uint8Array(lastRegistrationRequest.excludedCredentials[0]), + ) + ); + }); + + this.containsExcludedCiphers = excludedCiphers.length > 0; + this.ciphersSubject.next(excludedCiphers); + } else { + const relevantCiphers = ciphers.filter((cipher) => { + const credentialId = cipher.login.hasFido2Credentials + ? Array.from(parseCredentialId(cipher.login.fido2Credentials[0]?.credentialId)) + : []; + if (!cipher.login || !cipher.login.hasUris) { + return false; + } + + return ( + cipher.login.matchesUri(rpid, equivalentDomains) && + !lastRegistrationRequest.excludedCredentials.includes(credentialId) + ); + }); + this.ciphersSubject.next(relevantCiphers); + } }) .catch((error) => this.logService.error(error)); } @@ -99,11 +133,21 @@ export class Fido2CreateComponent implements OnInit, OnDestroy { } async addPasskeyToCipher(cipher: CipherView) { - const userVerified = cipher.reprompt - ? await this.passwordRepromptService.showPasswordPrompt() - : true; + let isConfirmed = true; - this.session.notifyConfirmCreateCredential(userVerified, cipher); + if (cipher.login.hasFido2Credentials) { + isConfirmed = await this.dialogService.openSimpleDialog({ + title: { key: "overwritePasskey" }, + content: { key: "alreadyContainsPasskey" }, + type: "warning", + }); + } + + if (cipher.reprompt) { + isConfirmed = await this.passwordRepromptService.showPasswordPrompt(); + } + + this.session.notifyConfirmCreateCredential(isConfirmed, cipher); } async confirmPasskey() { diff --git a/apps/desktop/src/modal/passkeys/fido2-vault.component.html b/apps/desktop/src/modal/passkeys/fido2-vault.component.html index 5191dcb1b6e..533269a62d7 100644 --- a/apps/desktop/src/modal/passkeys/fido2-vault.component.html +++ b/apps/desktop/src/modal/passkeys/fido2-vault.component.html @@ -22,6 +22,7 @@ +
{{ "passkeyAlreadyExists" | i18n }}