diff --git a/apps/desktop/src/app/components/fido2placeholder.component.ts b/apps/desktop/src/app/components/fido2placeholder.component.ts index a73a757660f..00c3499be43 100644 --- a/apps/desktop/src/app/components/fido2placeholder.component.ts +++ b/apps/desktop/src/app/components/fido2placeholder.component.ts @@ -1,9 +1,7 @@ import { Component } from "@angular/core"; import { Router } from "@angular/router"; -import { - Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction -} from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction"; +import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction"; import { DesktopFido2UserInterfaceService } from "../../autofill/services/desktop-fido2-user-interface.service"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; @@ -45,31 +43,35 @@ export class Fido2PlaceholderComponent { ) {} async confirmPasskey() { - // placeholder, actual api arguments needed here should be discussed - // just show casing we can call into the session to create the credential or change it. + const desktopUiService = this.fido2UserInterfaceService as DesktopFido2UserInterfaceService; + console.log("Got desktopService", desktopUiService.guid); - console.log("checking for session", this.fido2UserInterfaceService); + try { + console.log("checking for session", this.fido2UserInterfaceService); + // Add timeout to avoid infinite hanging + const session = desktopUiService.getCurrentSession(); + if (!session) { + // todo: handle error + console.error("No session found"); + return; + } + console.log("Got session", session.guid); - const desktopService = this.fido2UserInterfaceService as DesktopFido2UserInterfaceService; + // const cipher = await session.createCredential({ + // userHandle: "userHandle2", + // userName: "username2", + // credentialName: "zxsd2", + // rpId: "webauthn.io", + // userVerification: true, + // }); - const session = await desktopService.getCurrentSession(); - - console.log("Got session", session); - - - await session.createCredential({ - userHandle: "userHandle", - userName: "", - credentialName: "", - rpId: "", - userVerification: true, - }); - - console.log("Created credential, will notify complete"); - session.notifyOperationCompleted(); - - await this.router.navigate(["/"]); - await this.desktopSettingsService.setInModalMode(false); + session.notifyOperationCompleted(); + await this.router.navigate(["/"]); + await this.desktopSettingsService.setInModalMode(false); + } catch (error) { + console.error("Failed during confirmation:", error); + // Handle error appropriately + } } async closeModal() { diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index e5b0b96a3e3..37fd9b56187 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { APP_INITIALIZER, NgModule } from "@angular/core"; +import { Router } from "@angular/router"; import { Subject, merge } from "rxjs"; import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; @@ -331,6 +332,8 @@ const safeProviders: SafeProvider[] = [ AccountService, LogService, MessagingServiceAbstraction, + Router, + DesktopSettingsService, ], }), safeProvider({ diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index 6e4ff8eaad1..c40ba6eb4bd 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -1,4 +1,5 @@ -import { filter, firstValueFrom, map, shareReplay, Subject } from "rxjs"; +import { Router } from "@angular/router"; +import { filter, lastValueFrom, firstValueFrom, map, shareReplay, Subject, tap } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; @@ -13,12 +14,17 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherRepromptType, CipherType, SecureNoteType } from "@bitwarden/common/vault/enums"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"; import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; +import { UsernameAlgorithms } from "@bitwarden/generator-core"; +import { DesktopSettingsService } from "src/platform/services/desktop-settings.service"; + +// import the angular router export class DesktopFido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction @@ -29,22 +35,17 @@ export class DesktopFido2UserInterfaceService private accountService: AccountService, private logService: LogService, private messagingService: MessagingService, - ) {} - - private currentSessionSubject = new Subject(); - private currentSubject$ = this.currentSessionSubject.pipe( - shareReplay({ refCount: true, bufferSize: 1 }), - filter(c => c !== undefined) - ); - - setCurrentSession(session: DesktopFido2UserInterfaceSession) { - this.currentSessionSubject.next(session); - } - - getCurrentSession(): Promise { - return firstValueFrom(this.currentSubject$); + private router: Router, + private desktopSettingsService: DesktopSettingsService, + ) { + this.guid = "PARENT" + Math.random().toString(36).substring(7); } + guid: string; + private currentSession: any; + getCurrentSession(): DesktopFido2UserInterfaceSession | undefined { + return this.currentSession; + } async newSession( fallbackSupported: boolean, @@ -52,13 +53,21 @@ export class DesktopFido2UserInterfaceService abortController?: AbortController, ): Promise { this.logService.warning("newSession", fallbackSupported, abortController); - return new DesktopFido2UserInterfaceSession( + const session = new DesktopFido2UserInterfaceSession( this.authService, this.cipherService, this.accountService, this.logService, this.messagingService, + this.router, + this.desktopSettingsService, ); + + console.log("In parent", this.guid); + console.log("Setting current session", session.guid); + + this.currentSession = session; + return session; } } @@ -69,17 +78,42 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi private accountService: AccountService, private logService: LogService, private messagingService: MessagingService, - ) {} - - async pickCredential({ - cipherIds, - userVerification, - }: PickCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> { - this.logService.warning("pickCredential", cipherIds, userVerification); - - return { cipherId: cipherIds[0], userVerified: userVerification }; + private router: Router, + private desktopSettingsService: DesktopSettingsService, + ) { + this.guid = "SESSION" + Math.random().toString(36).substring(7); } + guid: string; + + pickCredential: ( + params: PickCredentialParams, + ) => Promise<{ cipherId: string; userVerified: boolean }>; + + private operationSubject = new Subject(); + private createdCipher: Cipher; + + /** + * Notifies the Fido2UserInterfaceSession that the UI operations has completed and it can return to the OS. + */ + notifyOperationCompleted() { + this.operationSubject.next(); + this.operationSubject.complete(); + } + + /** + * Returns once the UI has confirmed and completed the operation + * @returns + */ + private async waitForUICompletion(): Promise { + return lastValueFrom(this.operationSubject); + } + + /** + * This is called by the OS. It loads the UI and waits for the user to confirm the new credential. Once the UI has confirmed, it returns to the the OS. + * @param param0 + * @returns + */ async confirmNewCredential({ credentialName, userName, @@ -94,8 +128,40 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi rpId, ); - this.messagingService.send("loadurl", { url: "/passkeys", modal: true }); + try { + // Load the UI: + // maybe this modal mode thing shouldn't be done here? + await this.desktopSettingsService.setInModalMode(true); + await this.router.navigate(["/passkeys"]); + // Wait for the UI to wrap up + await this.waitForUICompletion(); + await this.createCredential({ + credentialName, + userName, + rpId, + userHandle: "", + userVerification, + }); + console.log("Returning from confirmNewCredential, created cipher:", this.createdCipher); + + // wait for 10ms to help RXJS catch up(?) + // We sometimes get a race condition from this.createCredential not updating cipherService in time + console.log("waiting 10ms.."); + await new Promise((resolve) => setTimeout(resolve, 10)); + console.log("Just waited 10ms"); + // Return the new cipher (this.createdCipher) + return { cipherId: this.createdCipher.id, userVerified: userVerification }; + } finally { + await this.desktopSettingsService.setInModalMode(false); + } + } + + /** + * Can be called by the UI to create a new credential with user input etc. + * @param param0 + */ + async createCredential({ credentialName, userName, rpId }: NewCredentialParams): Promise { // Store the passkey on a new cipher to avoid replacing something important const cipher = new CipherView(); cipher.name = credentialName; @@ -118,7 +184,9 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi const encCipher = await this.cipherService.encrypt(cipher, activeUserId); const createdCipher = await this.cipherService.createWithServer(encCipher); - return { cipherId: createdCipher.id, userVerified: userVerification }; + this.createdCipher = createdCipher; + + return createdCipher; } async informExcludedCredential(existingCipherIds: string[]): Promise { diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts index 376f4dcdced..86f563b7c91 100644 --- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts +++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts @@ -132,6 +132,7 @@ export class Fido2AuthenticatorService userVerification: params.requireUserVerification, rpId: params.rpEntity.id, }); + console.log("rpid", params.rpEntity.id, response.cipherId); const cipherId = response.cipherId; userVerified = response.userVerified; @@ -146,6 +147,7 @@ export class Fido2AuthenticatorService keyPair = await createKeyPair(); pubKeyDer = await crypto.subtle.exportKey("spki", keyPair.publicKey); const encrypted = await this.cipherService.get(cipherId); + console.log("Encrypted", encrypted); const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); @@ -153,6 +155,7 @@ export class Fido2AuthenticatorService cipher = await encrypted.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(encrypted, activeUserId), ); + if ( !userVerified && @@ -174,13 +177,15 @@ export class Fido2AuthenticatorService await this.cipherService.updateWithServer(reencrypted); await this.cipherService.clearCache(activeUserId); credentialId = fido2Credential.credentialId; + console.log("rpid", params.rpEntity.id); + } catch (error) { this.logService?.error( `[Fido2Authenticator] Aborting because of unknown error when creating credential: ${error}`, ); throw new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.Unknown); } - + console.log("authdata rpid", params.rpEntity.id); const authData = await generateAuthData({ rpId: params.rpEntity.id, credentialId: parseCredentialId(credentialId), diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 8711496b374..0f1b1fe93b4 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -10,6 +10,7 @@ import { shareReplay, Subject, switchMap, + tap, } from "rxjs"; import { SemVer } from "semver"; @@ -141,6 +142,7 @@ export class CipherService implements CipherServiceAbstraction { this.cipherViews$ = combineLatest([this.encryptedCiphersState.state$, this.localData$]).pipe( filter(([ciphers]) => ciphers != null), // Skip if ciphers haven't been loaded yor synced yet switchMap(() => merge(this.forceCipherViews$, this.getAllDecrypted())), + tap((v) => console.log("---- cipherViews$", v)), shareReplay({ bufferSize: 1, refCount: true }), ); this.addEditCipherInfo$ = this.addEditCipherInfoState.state$;