mirror of
https://github.com/bitwarden/browser
synced 2026-02-11 05:53:42 +00:00
Merge branch 'passkey-window-working' into autofill/PM-9034-implement-passkey-for-unlocked-accounts
This commit is contained in:
@@ -7,6 +7,16 @@ use crate::{BitwardenError, Callback, UserVerification};
|
||||
#[derive(uniffi::Record, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyAssertionRequest {
|
||||
rp_id: String,
|
||||
client_data_hash: Vec<u8>,
|
||||
user_verification: UserVerification,
|
||||
allowed_credentials: Vec<Vec<u8>>,
|
||||
//extension_input: Vec<u8>, TODO: Implement support for extensions
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyAssertionWithoutUserInterfaceRequest {
|
||||
rp_id: String,
|
||||
credential_id: Vec<u8>,
|
||||
user_name: String,
|
||||
|
||||
@@ -15,7 +15,10 @@ uniffi::setup_scaffolding!();
|
||||
mod assertion;
|
||||
mod registration;
|
||||
|
||||
use assertion::{PasskeyAssertionRequest, PreparePasskeyAssertionCallback};
|
||||
use assertion::{
|
||||
PasskeyAssertionRequest, PasskeyAssertionWithoutUserInterfaceRequest,
|
||||
PreparePasskeyAssertionCallback,
|
||||
};
|
||||
use registration::{PasskeyRegistrationRequest, PreparePasskeyRegistrationCallback};
|
||||
|
||||
#[derive(uniffi::Enum, Debug, Serialize, Deserialize)]
|
||||
@@ -141,6 +144,14 @@ impl MacOSProviderClient {
|
||||
) {
|
||||
self.send_message(request, Box::new(callback));
|
||||
}
|
||||
|
||||
pub fn prepare_passkey_assertion_without_user_interface(
|
||||
&self,
|
||||
request: PasskeyAssertionWithoutUserInterfaceRequest,
|
||||
callback: Arc<dyn PreparePasskeyAssertionCallback>,
|
||||
) {
|
||||
self.send_message(request, Box::new(callback));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
||||
8
apps/desktop/desktop_native/napi/index.d.ts
vendored
8
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -144,6 +144,12 @@ export declare namespace autofill {
|
||||
attestationObject: Array<number>
|
||||
}
|
||||
export interface PasskeyAssertionRequest {
|
||||
rpId: string
|
||||
clientDataHash: Array<number>
|
||||
userVerification: UserVerification
|
||||
allowedCredentials: Array<Array<number>>
|
||||
}
|
||||
export interface PasskeyAssertionWithoutUserInterfaceRequest {
|
||||
rpId: string
|
||||
credentialId: Array<number>
|
||||
userName: string
|
||||
@@ -167,7 +173,7 @@ export declare namespace autofill {
|
||||
* @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
|
||||
* @param callback This function will be called whenever a message is received from a client.
|
||||
*/
|
||||
static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void): Promise<IpcServer>
|
||||
static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void, assertionWithoutUserInterfaceCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void): Promise<IpcServer>
|
||||
/** Return the path to the IPC server. */
|
||||
getPath(): string
|
||||
/** Stop the IPC server. */
|
||||
|
||||
@@ -608,6 +608,17 @@ pub mod autofill {
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyAssertionRequest {
|
||||
pub rp_id: String,
|
||||
pub client_data_hash: Vec<u8>,
|
||||
pub user_verification: UserVerification,
|
||||
pub allowed_credentials: Vec<Vec<u8>>,
|
||||
//extension_input: Vec<u8>, TODO: Implement support for extensions
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyAssertionWithoutUserInterfaceRequest {
|
||||
pub rp_id: String,
|
||||
pub credential_id: Vec<u8>,
|
||||
pub user_name: String,
|
||||
@@ -659,6 +670,13 @@ pub mod autofill {
|
||||
(u32, u32, PasskeyAssertionRequest),
|
||||
ErrorStrategy::CalleeHandled,
|
||||
>,
|
||||
#[napi(
|
||||
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void"
|
||||
)]
|
||||
assertion_without_user_interface_callback: ThreadsafeFunction<
|
||||
(u32, u32, PasskeyAssertionWithoutUserInterfaceRequest),
|
||||
ErrorStrategy::CalleeHandled,
|
||||
>,
|
||||
) -> napi::Result<Self> {
|
||||
let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(32);
|
||||
tokio::spawn(async move {
|
||||
@@ -695,6 +713,25 @@ pub mod autofill {
|
||||
}
|
||||
}
|
||||
|
||||
match serde_json::from_str::<
|
||||
PasskeyMessage<PasskeyAssertionWithoutUserInterfaceRequest>,
|
||||
>(&message)
|
||||
{
|
||||
Ok(msg) => {
|
||||
let value = msg
|
||||
.value
|
||||
.map(|value| (client_id, msg.sequence_number, value))
|
||||
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
|
||||
|
||||
assertion_without_user_interface_callback
|
||||
.call(value, ThreadsafeFunctionCallMode::NonBlocking);
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[ERROR] Error deserializing message1: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
match serde_json::from_str::<PasskeyMessage<PasskeyRegistrationRequest>>(
|
||||
&message,
|
||||
) {
|
||||
|
||||
@@ -19,15 +19,15 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
// I think we still might want a static regardless, to be able to reuse the connection if possible.
|
||||
static let client: MacOsProviderClient = {
|
||||
let instance = MacOsProviderClient.connect()
|
||||
// setup code
|
||||
return instance
|
||||
}()
|
||||
// setup code
|
||||
return instance
|
||||
}()
|
||||
|
||||
init() {
|
||||
logger = Logger(subsystem: "com.bitwarden.desktop.autofill-extension", category: "credential-provider")
|
||||
|
||||
logger.log("[autofill-extension] initializing extension")
|
||||
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@@ -37,18 +37,19 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
|
||||
deinit {
|
||||
logger.log("[autofill-extension] deinitializing extension")
|
||||
self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue))
|
||||
}
|
||||
|
||||
|
||||
@IBAction func cancel(_ sender: AnyObject?) {
|
||||
self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue))
|
||||
}
|
||||
|
||||
|
||||
@IBAction func passwordSelected(_ sender: AnyObject?) {
|
||||
let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234")
|
||||
self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil)
|
||||
}
|
||||
|
||||
|
||||
override func loadView() {
|
||||
let view = NSView()
|
||||
view.isHidden = true
|
||||
@@ -63,9 +64,9 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
Provide the password by completing the extension request with the associated ASPasswordCredential.
|
||||
If using the credential would require showing custom UI for authenticating the user, cancel
|
||||
the request with error code ASExtensionError.userInteractionRequired.
|
||||
|
||||
|
||||
*/
|
||||
|
||||
|
||||
// Deprecated
|
||||
override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) {
|
||||
logger.log("[autofill-extension] provideCredentialWithoutUserInteraction called \(credentialIdentity)")
|
||||
@@ -74,19 +75,19 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
logger.log("[autofill-extension] sid \(credentialIdentity.serviceIdentifier.identifier)")
|
||||
logger.log("[autofill-extension] sidt \(credentialIdentity.serviceIdentifier.type.rawValue)")
|
||||
|
||||
// let databaseIsUnlocked = true
|
||||
// if (databaseIsUnlocked) {
|
||||
// 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))
|
||||
// }
|
||||
self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil)
|
||||
// } else {
|
||||
// self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code:ASExtensionError.userInteractionRequired.rawValue))
|
||||
// }
|
||||
}
|
||||
|
||||
override func provideCredentialWithoutUserInteraction(for credentialRequest: any ASCredentialRequest) {
|
||||
|
||||
|
||||
//logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2(credentialRequest) called \(request)")
|
||||
|
||||
|
||||
if let request = credentialRequest as? ASPasskeyCredentialRequest {
|
||||
if let passkeyIdentity = request.credentialIdentity as? ASPasskeyCredentialIdentity {
|
||||
|
||||
@@ -94,8 +95,10 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
|
||||
class CallbackImpl: PreparePasskeyAssertionCallback {
|
||||
let ctx: ASCredentialProviderExtensionContext
|
||||
required init(_ ctx: ASCredentialProviderExtensionContext) {
|
||||
let logger: Logger
|
||||
required init(_ ctx: ASCredentialProviderExtensionContext,_ logger: Logger) {
|
||||
self.ctx = ctx
|
||||
self.logger = logger
|
||||
}
|
||||
|
||||
func onComplete(credential: PasskeyAssertionResponse) {
|
||||
@@ -109,6 +112,102 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
))
|
||||
}
|
||||
|
||||
func onError(error: BitwardenError) {
|
||||
logger.log("[autofill-extension] ERROR HAPPENED in swift error \(error)")
|
||||
ctx.cancelRequest(withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
let userVerification = switch request.userVerificationPreference {
|
||||
case .preferred:
|
||||
UserVerification.preferred
|
||||
case .required:
|
||||
UserVerification.required
|
||||
default:
|
||||
UserVerification.discouraged
|
||||
}
|
||||
|
||||
let req = PasskeyAssertionWithoutUserInterfaceRequest(
|
||||
rpId: passkeyIdentity.relyingPartyIdentifier,
|
||||
credentialId: passkeyIdentity.credentialID,
|
||||
userName: passkeyIdentity.userName,
|
||||
userHandle: passkeyIdentity.userHandle,
|
||||
recordIdentifier: passkeyIdentity.recordIdentifier,
|
||||
clientDataHash: request.clientDataHash,
|
||||
userVerification: userVerification
|
||||
)
|
||||
|
||||
CredentialProviderViewController.client.preparePasskeyAssertionWithoutUserInterface(request: req, callback: CallbackImpl(self.extensionContext, self.logger))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if let request = credentialRequest as? ASPasswordCredentialRequest {
|
||||
logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2(password) called \(request)")
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2 called wrong")
|
||||
self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Invalid authentication request"))
|
||||
}
|
||||
|
||||
/*
|
||||
Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with
|
||||
ASExtensionError.userInteractionRequired. In this case, the system may present your extension's
|
||||
UI and call this method. Show appropriate UI for authenticating the user then provide the password
|
||||
by completing the extension request with the associated ASPasswordCredential.
|
||||
|
||||
override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) {
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
override func prepareInterfaceForExtensionConfiguration() {
|
||||
logger.log("[autofill-extension] prepareInterfaceForExtensionConfiguration called")
|
||||
}
|
||||
|
||||
override func prepareInterface(forPasskeyRegistration registrationRequest: ASCredentialRequest) {
|
||||
logger.log("[autofill-extension] prepareInterface")
|
||||
|
||||
// Create a timer for 20 second timeout
|
||||
let timeoutTimer = DispatchWorkItem { [weak self] in
|
||||
guard let self = self else { return }
|
||||
logger.log("[autofill-extension] Registration timed out after 20 seconds")
|
||||
self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Registration timed out"))
|
||||
}
|
||||
|
||||
// Schedule the timeout
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 20, execute: timeoutTimer)
|
||||
|
||||
// Create a timer to show UI after 10 seconds
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
// Configure and show UI elements for manual cancellation
|
||||
self.configureTimeoutUI()
|
||||
}
|
||||
|
||||
if let request = registrationRequest as? ASPasskeyCredentialRequest {
|
||||
if let passkeyIdentity = registrationRequest.credentialIdentity as? ASPasskeyCredentialIdentity {
|
||||
logger.log("[autofill-extension] prepareInterface(passkey) called \(request)")
|
||||
|
||||
class CallbackImpl: PreparePasskeyRegistrationCallback {
|
||||
let ctx: ASCredentialProviderExtensionContext
|
||||
|
||||
required init(_ ctx: ASCredentialProviderExtensionContext) {
|
||||
self.ctx = ctx
|
||||
}
|
||||
|
||||
func onComplete(credential: PasskeyRegistrationResponse) {
|
||||
|
||||
|
||||
ctx.completeRegistrationRequest(using: ASPasskeyRegistrationCredential(
|
||||
relyingParty: credential.rpId,
|
||||
clientDataHash: credential.clientDataHash,
|
||||
credentialID: credential.credentialId,
|
||||
attestationObject: credential.attestationObject
|
||||
))
|
||||
}
|
||||
|
||||
func onError(error: BitwardenError) {
|
||||
ctx.cancelRequest(withError: error)
|
||||
}
|
||||
@@ -123,88 +222,6 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
UserVerification.discouraged
|
||||
}
|
||||
|
||||
let req = PasskeyAssertionRequest(
|
||||
rpId: passkeyIdentity.relyingPartyIdentifier,
|
||||
credentialId: passkeyIdentity.credentialID,
|
||||
userName: passkeyIdentity.userName,
|
||||
userHandle: passkeyIdentity.userHandle,
|
||||
recordIdentifier: passkeyIdentity.recordIdentifier,
|
||||
clientDataHash: request.clientDataHash,
|
||||
userVerification: userVerification
|
||||
)
|
||||
|
||||
CredentialProviderViewController.client.preparePasskeyAssertion(request: req, callback: CallbackImpl(self.extensionContext))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if let request = credentialRequest as? ASPasswordCredentialRequest {
|
||||
logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2(password) called \(request)")
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2 called wrong")
|
||||
self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Invalid authentication request"))
|
||||
}
|
||||
|
||||
/*
|
||||
Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with
|
||||
ASExtensionError.userInteractionRequired. In this case, the system may present your extension's
|
||||
UI and call this method. Show appropriate UI for authenticating the user then provide the password
|
||||
by completing the extension request with the associated ASPasswordCredential.
|
||||
|
||||
override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) {
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
override func prepareInterfaceForExtensionConfiguration() {
|
||||
logger.log("[autofill-extension] prepareInterfaceForExtensionConfiguration called")
|
||||
}
|
||||
|
||||
override func prepareInterface(forPasskeyRegistration registrationRequest: ASCredentialRequest) {
|
||||
logger.log("[autofill-extension] prepareInterface")
|
||||
|
||||
// Create a timer to show UI after 10 seconds
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
// Configure and show UI elements for manual cancellation
|
||||
self.configureTimeoutUI()
|
||||
}
|
||||
|
||||
if let request = registrationRequest as? ASPasskeyCredentialRequest {
|
||||
if let passkeyIdentity = registrationRequest.credentialIdentity as? ASPasskeyCredentialIdentity {
|
||||
logger.log("[autofill-extension] prepareInterface(passkey) called \(request)")
|
||||
|
||||
class CallbackImpl: PreparePasskeyRegistrationCallback {
|
||||
let ctx: ASCredentialProviderExtensionContext
|
||||
required init(_ ctx: ASCredentialProviderExtensionContext) {
|
||||
self.ctx = ctx
|
||||
}
|
||||
|
||||
func onComplete(credential: PasskeyRegistrationResponse) {
|
||||
ctx.completeRegistrationRequest(using: ASPasskeyRegistrationCredential(
|
||||
relyingParty: credential.rpId,
|
||||
clientDataHash: credential.clientDataHash,
|
||||
credentialID: credential.credentialId,
|
||||
attestationObject: credential.attestationObject
|
||||
))
|
||||
}
|
||||
|
||||
func onError(error: BitwardenError) {
|
||||
ctx.cancelRequest(withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
let userVerification = switch request.userVerificationPreference {
|
||||
case .preferred:
|
||||
UserVerification.preferred
|
||||
case .required:
|
||||
UserVerification.required
|
||||
default:
|
||||
UserVerification.discouraged
|
||||
}
|
||||
|
||||
let req = PasskeyRegistrationRequest(
|
||||
rpId: passkeyIdentity.relyingPartyIdentifier,
|
||||
userName: passkeyIdentity.userName,
|
||||
@@ -217,31 +234,33 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
// Log details of the request
|
||||
logger.log("[autofill-extension] rpId: \(req.rpId)")
|
||||
logger.log("[autofill-extension] rpId: \(req.userName)")
|
||||
|
||||
|
||||
|
||||
CredentialProviderViewController.client.preparePasskeyRegistration(request: req, callback: CallbackImpl(self.extensionContext))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
logger.log("[autofill-extension] We didn't get a passkey")
|
||||
|
||||
|
||||
timeoutTimer.cancel()
|
||||
// If we didn't get a passkey, return an error
|
||||
self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Invalid registration request"))
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Prepare your UI to list available credentials for the user to choose from. The items in
|
||||
'serviceIdentifiers' describe the service the user is logging in to, so your extension can
|
||||
prioritize the most relevant credentials in the list.
|
||||
Prepare your UI to list available credentials for the user to choose from. The items in
|
||||
'serviceIdentifiers' describe the service the user is logging in to, so your extension can
|
||||
prioritize the most relevant credentials in the list.
|
||||
*/
|
||||
override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) {
|
||||
logger.log("[autofill-extension] prepareCredentialList for serviceIdentifiers: \(serviceIdentifiers.count)")
|
||||
|
||||
|
||||
for serviceIdentifier in serviceIdentifiers {
|
||||
logger.log(" service: \(serviceIdentifier.identifier)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier], requestParameters: ASPasskeyCredentialRequestParameters) {
|
||||
logger.log("[autofill-extension] prepareCredentialList(passkey) for serviceIdentifiers: \(serviceIdentifiers.count)")
|
||||
logger.log("request parameters: \(requestParameters.relyingPartyIdentifier)")
|
||||
@@ -282,28 +301,20 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
UserVerification.discouraged
|
||||
}
|
||||
|
||||
// TODO: PasskeyAssertionRequest does not implement allowedCredentials, extensions and required credentialId, username, userhandle, recordIdentifier??
|
||||
let req = PasskeyAssertionRequest(
|
||||
rpId: requestParameters.relyingPartyIdentifier,
|
||||
|
||||
// TODO: remove once the PasskeyAssertionRequest type has been improved
|
||||
credentialId: Data(),
|
||||
userName: "",
|
||||
userHandle: Data(),
|
||||
recordIdentifier: "",
|
||||
|
||||
//allowedCredentials: requestParameters.allowedCredentials,
|
||||
//extensionInput: requestParameters.extensionInput,
|
||||
clientDataHash: requestParameters.clientDataHash,
|
||||
userVerification: userVerification
|
||||
userVerification: userVerification,
|
||||
allowedCredentials: requestParameters.allowedCredentials
|
||||
//extensionInput: requestParameters.extensionInput,
|
||||
)
|
||||
|
||||
CredentialProviderViewController.client.preparePasskeyAssertion(request: req, callback: CallbackImpl(self.extensionContext))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
private func configureTimeoutUI() {
|
||||
self.view.isHidden = false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -80,6 +80,44 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
ipcRenderer.send("autofill.completePasskeyAssertion", {
|
||||
clientId,
|
||||
sequenceNumber,
|
||||
response,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
listenPasskeyAssertionWithoutUserInterface: (
|
||||
fn: (
|
||||
clientId: number,
|
||||
sequenceNumber: number,
|
||||
request: autofill.PasskeyAssertionWithoutUserInterfaceRequest,
|
||||
completeCallback: (error: Error | null, response: autofill.PasskeyAssertionResponse) => void,
|
||||
) => void,
|
||||
) => {
|
||||
ipcRenderer.on(
|
||||
"autofill.passkeyAssertionWithoutUserInterface",
|
||||
(
|
||||
event,
|
||||
data: {
|
||||
clientId: number;
|
||||
sequenceNumber: number;
|
||||
request: autofill.PasskeyAssertionWithoutUserInterfaceRequest;
|
||||
},
|
||||
) => {
|
||||
const { clientId, sequenceNumber, request } = data;
|
||||
fn(clientId, sequenceNumber, request, (error, response) => {
|
||||
if (error) {
|
||||
ipcRenderer.send("autofill.completeError", {
|
||||
clientId,
|
||||
sequenceNumber,
|
||||
error: error.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ipcRenderer.send("autofill.completePasskeyAssertion", {
|
||||
clientId,
|
||||
sequenceNumber,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
map,
|
||||
mergeMap,
|
||||
switchMap,
|
||||
takeUntil
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -157,39 +157,56 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
});
|
||||
});
|
||||
|
||||
ipc.autofill.listenPasskeyAssertionWithoutUserInterface(
|
||||
async (clientId, sequenceNumber, request, callback) => {
|
||||
this.logService.warning("listenPasskeyAssertion", clientId, sequenceNumber, request);
|
||||
|
||||
// TODO: For some reason the credentialId is passed as an empty array in the request, so we need to
|
||||
// get it from the cipher. For that we use the recordIdentifier, which is the cipherId.
|
||||
if (request.recordIdentifier && request.credentialId.length === 0) {
|
||||
const cipher = await this.cipherService.get(request.recordIdentifier);
|
||||
if (!cipher) {
|
||||
this.logService.error("listenPasskeyAssertion error", "Cipher not found");
|
||||
callback(new Error("Cipher not found"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
const decrypted = await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
);
|
||||
|
||||
const fido2Credential = decrypted.login.fido2Credentials?.[0];
|
||||
if (!fido2Credential) {
|
||||
this.logService.error("listenPasskeyAssertion error", "Fido2Credential not found");
|
||||
callback(new Error("Fido2Credential not found"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
request.credentialId = Array.from(
|
||||
guidToRawFormat(decrypted.login.fido2Credentials?.[0].credentialId),
|
||||
);
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
void this.fido2AuthenticatorService
|
||||
.getAssertion(this.convertAssertionRequest(request), null, controller)
|
||||
.then((response) => {
|
||||
callback(null, this.convertAssertionResponse(request, response));
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logService.error("listenPasskeyAssertion error", error);
|
||||
callback(error, null);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
ipc.autofill.listenPasskeyAssertion(async (clientId, sequenceNumber, request, callback) => {
|
||||
this.logService.warning("listenPasskeyAssertion", clientId, sequenceNumber, request);
|
||||
|
||||
// TODO: For some reason the credentialId is passed as an empty array in the request, so we need to
|
||||
// get it from the cipher. For that we use the recordIdentifier, which is the cipherId.
|
||||
if (request.recordIdentifier && request.credentialId.length === 0) {
|
||||
const cipher = await this.cipherService.get(request.recordIdentifier);
|
||||
if (!cipher) {
|
||||
this.logService.error("listenPasskeyAssertion error", "Cipher not found");
|
||||
callback(new Error("Cipher not found"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
const decrypted = await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
);
|
||||
|
||||
const fido2Credential = decrypted.login.fido2Credentials?.[0];
|
||||
if (!fido2Credential) {
|
||||
this.logService.error("listenPasskeyAssertion error", "Fido2Credential not found");
|
||||
callback(new Error("Fido2Credential not found"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
request.credentialId = Array.from(
|
||||
guidToRawFormat(decrypted.login.fido2Credentials?.[0].credentialId),
|
||||
);
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
void this.fido2AuthenticatorService
|
||||
.getAssertion(this.convertAssertionRequest(request), null, controller)
|
||||
@@ -245,17 +262,29 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
}
|
||||
|
||||
private convertAssertionRequest(
|
||||
request: autofill.PasskeyAssertionRequest,
|
||||
request:
|
||||
| autofill.PasskeyAssertionRequest
|
||||
| autofill.PasskeyAssertionWithoutUserInterfaceRequest,
|
||||
): Fido2AuthenticatorGetAssertionParams {
|
||||
let allowedCredentials;
|
||||
if ("credentialId" in request) {
|
||||
allowedCredentials = [
|
||||
{
|
||||
id: new Uint8Array(request.credentialId),
|
||||
type: "public-key" as const,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
allowedCredentials = request.allowedCredentials.map((credentialId) => ({
|
||||
id: new Uint8Array(credentialId),
|
||||
type: "public-key" as const,
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
rpId: request.rpId,
|
||||
hash: new Uint8Array(request.clientDataHash),
|
||||
allowCredentialDescriptorList: [
|
||||
{
|
||||
id: new Uint8Array(request.credentialId),
|
||||
type: "public-key",
|
||||
},
|
||||
],
|
||||
allowCredentialDescriptorList: allowedCredentials,
|
||||
extensions: {},
|
||||
requireUserVerification:
|
||||
request.userVerification === "required" || request.userVerification === "preferred",
|
||||
@@ -264,7 +293,9 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
}
|
||||
|
||||
private convertAssertionResponse(
|
||||
request: autofill.PasskeyAssertionRequest,
|
||||
request:
|
||||
| autofill.PasskeyAssertionRequest
|
||||
| autofill.PasskeyAssertionWithoutUserInterfaceRequest,
|
||||
response: Fido2AuthenticatorGetAssertionResult,
|
||||
): autofill.PasskeyAssertionResponse {
|
||||
return {
|
||||
|
||||
@@ -60,6 +60,18 @@ export class NativeAutofillMain {
|
||||
request,
|
||||
});
|
||||
},
|
||||
// AssertionWihtoutUserInterfaceCallback
|
||||
(error, clientId, sequenceNumber, request) => {
|
||||
if (error) {
|
||||
this.logService.error("autofill.IpcServer.assertion", error);
|
||||
return;
|
||||
}
|
||||
this.windowMain.win.webContents.send("autofill.passkeyAssertionWithoutUserInterface", {
|
||||
clientId,
|
||||
sequenceNumber,
|
||||
request,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.on("autofill.completePasskeyRegistration", (event, data) => {
|
||||
@@ -77,7 +89,7 @@ export class NativeAutofillMain {
|
||||
ipcMain.on("autofill.completeError", (event, data) => {
|
||||
this.logService.warning("autofill.completeError", data);
|
||||
const { clientId, sequenceNumber, error } = data;
|
||||
this.ipcServer.completeAssertion(clientId, sequenceNumber, error);
|
||||
this.ipcServer.completeError(clientId, sequenceNumber, error);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user