From 7a43d5c11fb2c1658b1593022ffb4c384b3020b8 Mon Sep 17 00:00:00 2001 From: Amatsugu Date: Sat, 11 Apr 2026 18:56:04 -0400 Subject: [PATCH] wip passkey registration --- AobaCore/Models/User.cs | 5 +++- AobaCore/Services/AccountsService.cs | 13 +++++++-- AobaServer/Program.cs | 10 +++++++ AobaServer/Proto/Account.proto | 4 +-- AobaServer/Proto/Types.proto | 16 +++++++++-- AobaServer/Services/AccountRpcService.cs | 35 +++++++++++++++++++++--- 6 files changed, 70 insertions(+), 13 deletions(-) diff --git a/AobaCore/Models/User.cs b/AobaCore/Models/User.cs index e8bbe46..12c4f4d 100644 --- a/AobaCore/Models/User.cs +++ b/AobaCore/Models/User.cs @@ -1,4 +1,6 @@ -using Microsoft.IdentityModel.Tokens; +using Fido2NetLib.Objects; + +using Microsoft.IdentityModel.Tokens; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; @@ -19,6 +21,7 @@ public class User public bool IsArgon { get; set; } public ObjectId[] ApiKeys { get; set; } = []; public List RegTokens { get; set; } = []; + public List CredentialDescriptors { get; set; } = []; public ClaimsIdentity GetIdentity() { diff --git a/AobaCore/Services/AccountsService.cs b/AobaCore/Services/AccountsService.cs index 1ea6329..165a3b0 100644 --- a/AobaCore/Services/AccountsService.cs +++ b/AobaCore/Services/AccountsService.cs @@ -1,5 +1,7 @@ using AobaCore.Models; +using Fido2NetLib.Objects; + using Isopoh.Cryptography.Argon2; using MongoDB.Bson; @@ -54,13 +56,18 @@ public class AccountsService(IMongoDatabase db) /* Get the salt */ byte[] salt = new byte[16]; Array.Copy(hashBytes, 0, salt, 0, 16); - /* Compute the hash on the password the user entered */ - var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 10000, HashAlgorithmName.SHA1); - byte[] hash = pbkdf2.GetBytes(20); + + var hash= Rfc2898DeriveBytes.Pbkdf2(password, salt, 10000, HashAlgorithmName.SHA1, 20); /* Compare the results */ for (int i = 0; i < 20; i++) if (hashBytes[i + 16] != hash[i]) return false; return true; } + + public async Task> GetPublicKeyCredentialDescriptorsAsync(ObjectId id, CancellationToken cancellationToken = default) + { + var creds = await _users.Find(u => u.Id == id).Project(u => u.CredentialDescriptors).FirstOrDefaultAsync(cancellationToken); + return creds ?? []; + } } diff --git a/AobaServer/Program.cs b/AobaServer/Program.cs index 9adb4f9..3957290 100644 --- a/AobaServer/Program.cs +++ b/AobaServer/Program.cs @@ -121,6 +121,16 @@ builder.Services.AddAuthentication(options => builder.Services.AddAoba(); +builder.Services.AddFido2(opts => +{ + opts.ServerName = "Aoba"; + opts.ServerDomain = "aoba.app"; +#if DEBUG + opts.Origins = new HashSet { "http://localhost:8081", "http://127.0.0.1:8080" }; +#else + opts.Origins = new HashSet { "https://aoba.app" }; +#endif +}); #if DEBUG builder.Services.AddHostedService(); #endif diff --git a/AobaServer/Proto/Account.proto b/AobaServer/Proto/Account.proto index f9b878a..37fab28 100644 --- a/AobaServer/Proto/Account.proto +++ b/AobaServer/Proto/Account.proto @@ -7,7 +7,7 @@ import "google/protobuf/empty.proto"; import "Proto/Types.proto"; service AccountRpc { - rpc RegisterPasskey(google.protobuf.Empty) returns (PasskeyRegistrationCreds); - rpc CompletePasskeyRegistration(PasskeyPublicKey) returns (google.protobuf.Empty); + rpc RegisterPasskey(google.protobuf.Empty) returns (PasskeyCredentialCreateOptions); + rpc CompletePasskeyRegistration(PasskeyRegistrationCredentials) returns (google.protobuf.Empty); } diff --git a/AobaServer/Proto/Types.proto b/AobaServer/Proto/Types.proto index 00fd441..7ccb687 100644 --- a/AobaServer/Proto/Types.proto +++ b/AobaServer/Proto/Types.proto @@ -121,9 +121,19 @@ message PasskeyPayload { } -message PasskeyRegistrationCreds{ - +message PasskeyCredentialCreateOptions{ + string challenge = 1; + string userId = 2; +} +message PasskeyRegistrationCredentials{ + string id = 1; + string rawId = 2; + string type = 3; + CredentialsClientResponse response = 4; } -message PasskeyPublicKey{ +message CredentialsClientResponse{ + string clientDataJSON = 1; + string attestationObject = 2; + string authenticatorData = 3; } \ No newline at end of file diff --git a/AobaServer/Services/AccountRpcService.cs b/AobaServer/Services/AccountRpcService.cs index b8f3cd0..59c93fa 100644 --- a/AobaServer/Services/AccountRpcService.cs +++ b/AobaServer/Services/AccountRpcService.cs @@ -1,20 +1,47 @@ using Aoba.RPC; using Aoba.RPC.Account; +using AobaCore.Services; + +using AobaServer.Utils; + +using Fido2NetLib; + using Google.Protobuf.WellKnownTypes; using Grpc.Core; +using Isopoh.Cryptography.Argon2; + namespace AobaServer.Services; -public class AccountRpcService : AccountRpc.AccountRpcBase +public class AccountRpcService(IFido2 fido2, AccountsService accounts) : AccountRpc.AccountRpcBase { - public override Task RegisterPasskey(Empty request, ServerCallContext context) + public override async Task RegisterPasskey(Empty request, ServerCallContext context) { - return base.RegisterPasskey(request, context); + var curUser = await accounts.GetUserAsync(context.GetUserId(), context.CancellationToken); + if (curUser == null) + throw new Exception($"Logged in user does not exist somehow. Id: {context.GetUserId()}"); + var user = new Fido2User + { + DisplayName = curUser.Username, + Id = curUser.Id.ToByteArray(), + Name = curUser.Username + }; + + var credOptions = fido2.RequestNewCredential(new RequestNewCredentialParams + { + User = user, + ExcludeCredentials = curUser.CredentialDescriptors + }); + return new PasskeyCredentialCreateOptions + { + Challenge = credOptions.Challenge.ToB64String().Replace('+', '-').Replace('/', '_'), + UserId = credOptions.User.Id.ToB64String().Replace('+', '-').Replace('/', '_') + }; } - public override Task CompletePasskeyRegistration(PasskeyPublicKey request, ServerCallContext context) + public override Task CompletePasskeyRegistration(PasskeyRegistrationCredentials request, ServerCallContext context) { return base.CompletePasskeyRegistration(request, context); }