From 8ff4fa74e4dcaefe8680a8b5d4da995a268ead01 Mon Sep 17 00:00:00 2001 From: Amatsugu Date: Sun, 12 Apr 2026 16:59:40 -0400 Subject: [PATCH] wip passkey registration day 2 --- AobaClient/Cargo.lock | 36 +++++++------- AobaClient/Cargo.toml | 6 ++- AobaClient/build.rs | 13 +++-- AobaClient/src/components/passkey.rs | 62 +++++++++++++++++++----- AobaClient/src/rpc.rs | 19 +++++++- AobaServer/Proto/Types.proto | 31 +++++++++++- AobaServer/Services/AccountRpcService.cs | 15 +++--- AobaServer/Utils/PasskeyExtensions.cs | 55 +++++++++++++++++++++ 8 files changed, 192 insertions(+), 45 deletions(-) create mode 100644 AobaServer/Utils/PasskeyExtensions.cs diff --git a/AobaClient/Cargo.lock b/AobaClient/Cargo.lock index 4db3efa..85136d6 100644 --- a/AobaClient/Cargo.lock +++ b/AobaClient/Cargo.lock @@ -24,8 +24,10 @@ dependencies = [ "dioxus", "dioxus-primitives", "dotenv", + "js-sys", "prost", "serde", + "serde-wasm-bindgen", "serde_repr", "tonic", "tonic-build", @@ -1669,10 +1671,12 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -3340,9 +3344,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -3353,23 +3357,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3377,9 +3377,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -3390,9 +3390,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -3412,9 +3412,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/AobaClient/Cargo.toml b/AobaClient/Cargo.toml index eb9bd29..25eafb8 100644 --- a/AobaClient/Cargo.toml +++ b/AobaClient/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" [dependencies] dioxus = { version = "0.7.5", features = ["router"] } -serde = "1.0.228" +serde = { version = "1.0.228", features = ["derive"] } serde_repr = "0.1.20" tonic = { version = "*", default-features = false, features = [ "codegen", @@ -15,8 +15,10 @@ tonic = { version = "*", default-features = false, features = [ ] } prost = "0.13" tonic-web-wasm-client = "0.7" -web-sys = { version = "0.3.91", features = ["Storage", "Window", "Navigator", "CredentialsContainer", "CredentialCreationOptions"] } +web-sys = { version = "0.3.91", features = ["Storage", "Window", "Navigator", "CredentialsContainer", "CredentialCreationOptions", "AttestationConveyancePreference", "PublicKeyCredentialCreationOptions", "PublicKeyCredentialRpEntity", "PublicKeyCredentialUserEntity"] } dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false } +serde-wasm-bindgen = "0.6.5" +js-sys = "0.3.95" [build-dependencies] tonic-build = { version = "*", default-features = false, features = ["prost"] } diff --git a/AobaClient/build.rs b/AobaClient/build.rs index 53c244b..33acf20 100644 --- a/AobaClient/build.rs +++ b/AobaClient/build.rs @@ -3,13 +3,15 @@ use std::env; use std::fs::File; use std::io::Write; -fn main() -> Result<(), Box> { +fn main() -> Result<(), Box> +{ tonic_build::configure() .build_server(false) .build_client(true) .compile_protos( &[ "../AobaServer/Proto/Aoba.proto", + "../AobaServer/Proto/Account.proto", "../AobaServer/Proto/Auth.proto", "../AobaServer/Proto/Metrics.proto", "../AobaServer/Proto/Types.proto", @@ -20,15 +22,18 @@ fn main() -> Result<(), Box> { Ok(()) } -fn forward_env() { +fn forward_env() +{ let dest_path = "./src/env.rs"; let mut f = File::create(&dest_path).unwrap(); f.write_all(b"// This file is automatically generated by build.rs\n\n") .unwrap(); dotenv().ok(); - for (key, value) in env::vars() { - if key.starts_with("APP_") { + for (key, value) in env::vars() + { + if key.starts_with("APP_") + { f.write_all("#[allow(dead_code)]\n".as_bytes()).unwrap(); let line = format!( "pub const {}: &'static str = \"{}\";\n", diff --git a/AobaClient/src/components/passkey.rs b/AobaClient/src/components/passkey.rs index 380ce5f..fcd350f 100644 --- a/AobaClient/src/components/passkey.rs +++ b/AobaClient/src/components/passkey.rs @@ -1,10 +1,15 @@ use dioxus::prelude::*; -use web_sys::{CredentialCreationOptions, window}; +use js_sys::{Uint8Array, wasm_bindgen::JsValue}; +use web_sys::{ + CredentialCreationOptions, PublicKeyCredentialCreationOptions, PublicKeyCredentialRpEntity, + PublicKeyCredentialUserEntity, window, +}; -use crate::components::basic::Button; +use crate::{components::basic::Button, rpc::aoba::PasskeyCredentialCreateOptions}; #[component] -pub fn PasskeyRegistrationButton() -> Element { +pub fn PasskeyRegistrationButton() -> Element +{ rsx! { Button{ text: "Register Passkey", @@ -15,22 +20,55 @@ pub fn PasskeyRegistrationButton() -> Element { } } -fn start_passkey_registration() { - create_credential(); +fn start_passkey_registration() +{ + create_credential(todo!()); } -fn create_credential() { - let credentials = window() - .expect("Failed to get window") - .navigator() - .credentials(); +fn create_credential(req_opts: PasskeyCredentialCreateOptions) +{ + let window = window().expect("Window does not exist"); + let credentaials = window.navigator().credentials(); + let opts = opts_from_rpc(req_opts); + + let result = credentaials.create_with_options(&opts); + todo!() +} + +fn opts_from_rpc(rpc_opts: PasskeyCredentialCreateOptions) -> CredentialCreationOptions +{ + let opt_user = &rpc_opts.user.expect("user is missing"); + let opt_rp = &rpc_opts.rp.expect("rp is missing"); let opts = CredentialCreationOptions::new(); - let _result = credentials.create_with_options(&opts); + let rp = PublicKeyCredentialRpEntity::new(&opt_rp.name); + rp.set_id(&opt_rp.id); + + let user = PublicKeyCredentialUserEntity::new_with_u8_array( + &opt_user.name, + &opt_user.display_name, + &to_u8_array(&opt_user.id), + ); + let pub_key_opts = PublicKeyCredentialCreationOptions::new_with_u8_array( + &to_u8_array(&rpc_opts.challenge), + &JsValue::undefined(), + &rp, + &user, + ); + //pub_key_opts.set_exclude_credentials(val); + opts.set_public_key(&pub_key_opts); + + return opts; +} + +fn to_u8_array(value: &String) -> Uint8Array +{ + todo!() } #[component] -pub fn PasskeyLoginButton() -> Element { +pub fn PasskeyLoginButton() -> Element +{ rsx! { Button{ text: "Login with Passkey" diff --git a/AobaClient/src/rpc.rs b/AobaClient/src/rpc.rs index 378bd8d..4426393 100644 --- a/AobaClient/src/rpc.rs +++ b/AobaClient/src/rpc.rs @@ -6,7 +6,9 @@ use tonic_web_wasm_client::Client; use crate::{ RPC_HOST, - rpc::aoba::{auth_rpc_client::AuthRpcClient, metrics_rpc_client::MetricsRpcClient}, + rpc::aoba::{ + account_rpc_client::AccountRpcClient, auth_rpc_client::AuthRpcClient, metrics_rpc_client::MetricsRpcClient, + }, }; pub mod aoba @@ -17,6 +19,7 @@ pub mod aoba static RPC_CLIENT: RpcConnection = RpcConnection { aoba: RwLock::new(None), auth: RwLock::new(None), + account: RwLock::new(None), metrics: RwLock::new(None), jwt: RwLock::new(None), }; @@ -26,6 +29,7 @@ pub struct RpcConnection { aoba: RwLock>>>, auth: RwLock>>, + account: RwLock>>>, metrics: RwLock>>>, jwt: RwLock>, } @@ -38,6 +42,12 @@ impl RpcConnection return self.aoba.read().unwrap().clone().unwrap(); } + pub fn get_account_client(&self) -> AccountRpcClient> + { + self.ensure_client(); + return self.account.read().unwrap().clone().unwrap(); + } + pub fn get_auth_client(&self) -> AuthRpcClient { self.ensure_client(); @@ -58,6 +68,8 @@ impl RpcConnection let aoba_client = AobaRpcClient::with_interceptor(wasm_client.clone(), AuthInterceptor); *self.aoba.write().unwrap() = Some(aoba_client); *self.auth.write().unwrap() = Some(AuthRpcClient::new(wasm_client.clone())); + *self.account.write().unwrap() = + Some(AccountRpcClient::with_interceptor(wasm_client.clone(), AuthInterceptor)); *self.metrics.write().unwrap() = Some(MetricsRpcClient::with_interceptor(wasm_client.clone(), AuthInterceptor)); } @@ -90,6 +102,11 @@ pub fn get_auth_rpc_client() -> AuthRpcClient return RPC_CLIENT.get_auth_client(); } +pub fn get_account_rpc_client() -> AccountRpcClient> +{ + return RPC_CLIENT.get_account_client(); +} + pub fn get_metrics_rpc_client() -> MetricsRpcClient> { return RPC_CLIENT.get_metrics_client(); diff --git a/AobaServer/Proto/Types.proto b/AobaServer/Proto/Types.proto index 7ccb687..02a452a 100644 --- a/AobaServer/Proto/Types.proto +++ b/AobaServer/Proto/Types.proto @@ -123,8 +123,28 @@ message PasskeyPayload { message PasskeyCredentialCreateOptions{ string challenge = 1; - string userId = 2; + PublicKeyCredentialUser user = 2; + PublicKeyCredentialRpEntity rp = 3; + repeated PubKeyCredParam pubKeyParams = 4; } + +message PubKeyCredParam{ + string alg = 1; + string type = 2; +} + +message PublicKeyCredentialRpEntity{ + string id = 1; + string icon = 2; + string name = 3; +} + +message PublicKeyCredentialUser{ + string id = 1; + string name = 2; + string displayName = 3; +} + message PasskeyRegistrationCredentials{ string id = 1; string rawId = 2; @@ -136,4 +156,11 @@ message CredentialsClientResponse{ string clientDataJSON = 1; string attestationObject = 2; string authenticatorData = 3; -} \ No newline at end of file +} + +message PublicKeyCredentialDescriptor{ + string type = 1; + string id = 2; + repeated string transports = 3; +} + diff --git a/AobaServer/Services/AccountRpcService.cs b/AobaServer/Services/AccountRpcService.cs index 59c93fa..19700d2 100644 --- a/AobaServer/Services/AccountRpcService.cs +++ b/AobaServer/Services/AccountRpcService.cs @@ -32,13 +32,16 @@ public class AccountRpcService(IFido2 fido2, AccountsService accounts) : Account var credOptions = fido2.RequestNewCredential(new RequestNewCredentialParams { User = user, - ExcludeCredentials = curUser.CredentialDescriptors + ExcludeCredentials = curUser.CredentialDescriptors, + AuthenticatorSelection = new AuthenticatorSelection + { + ResidentKey = Fido2NetLib.Objects.ResidentKeyRequirement.Required, + UserVerification = Fido2NetLib.Objects.UserVerificationRequirement.Preferred + } }); - return new PasskeyCredentialCreateOptions - { - Challenge = credOptions.Challenge.ToB64String().Replace('+', '-').Replace('/', '_'), - UserId = credOptions.User.Id.ToB64String().Replace('+', '-').Replace('/', '_') - }; + + + return credOptions.ToRPC(); } public override Task CompletePasskeyRegistration(PasskeyRegistrationCredentials request, ServerCallContext context) diff --git a/AobaServer/Utils/PasskeyExtensions.cs b/AobaServer/Utils/PasskeyExtensions.cs new file mode 100644 index 0000000..29a9451 --- /dev/null +++ b/AobaServer/Utils/PasskeyExtensions.cs @@ -0,0 +1,55 @@ +using Aoba.RPC; + +using Isopoh.Cryptography.Argon2; + +namespace AobaServer.Utils; + +public static class PasskeyExtensions +{ + public static PublicKeyCredentialRpEntity ToRPC(this Fido2NetLib.PublicKeyCredentialRpEntity value) + { + return new PublicKeyCredentialRpEntity + { + Id = value.Id, + Icon = value.Icon, + Name = value.Name, + }; + } + + public static PublicKeyCredentialUser ToRPC(this Fido2NetLib.Fido2User value) + { + return new PublicKeyCredentialUser + { + Id = value.Id.ToB64String(), + DisplayName = value.DisplayName, + Name = value.Name, + }; + } + + public static PubKeyCredParam ToRPC(this Fido2NetLib.PubKeyCredParam value) + { + return new PubKeyCredParam + { + Alg = value.Alg.ToString(), + Type = value.Type.ToString(), + }; + } + + public static IEnumerable ToRPC(this IEnumerable value) + { + return value.Select(x => x.ToRPC()); + } + + public static PasskeyCredentialCreateOptions ToRPC(this Fido2NetLib.CredentialCreateOptions value) + { + var opts = new PasskeyCredentialCreateOptions + { + Challenge = value.Challenge.ToB64String(), + Rp = value.Rp.ToRPC(), + User = value.User.ToRPC() + }; + //todo: excluded credentials + opts.PubKeyParams.AddRange(value.PubKeyCredParams.ToRPC()); + return opts; + } +}