using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Data; using BTCPayServer.Models; using BTCPayServer.U2F.Models; using Microsoft.AspNetCore.Identity; using NBitcoin; using U2F.Core.Models; using U2F.Core.Utils; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Query; namespace BTCPayServer.U2F { public class U2FService { private readonly ApplicationDbContextFactory _contextFactory; private ConcurrentDictionary> UserAuthenticationRequests { get; set; } = new ConcurrentDictionary>(); public U2FService(ApplicationDbContextFactory contextFactory) { _contextFactory = contextFactory; } public async Task> GetDevices(string userId) { using (var context = _contextFactory.CreateContext()) { return await context.U2FDevices .Where(device => device.ApplicationUserId == userId) .ToListAsync(); } } public async Task RemoveDevice(string id, string userId) { using (var context = _contextFactory.CreateContext()) { var device = await context.U2FDevices.FindAsync(id); if (device == null || !device.ApplicationUserId.Equals(userId, StringComparison.InvariantCulture)) { return; } context.U2FDevices.Remove(device); await context.SaveChangesAsync(); } } public async Task HasDevices(string userId) { using (var context = _contextFactory.CreateContext()) { return await context.U2FDevices.Where(fDevice => fDevice.ApplicationUserId == userId).AnyAsync(); } } public ServerRegisterResponse StartDeviceRegistration(string userId, string appId) { var startedRegistration = global::U2F.Core.Crypto.U2F.StartRegistration(appId); UserAuthenticationRequests.AddOrReplace(userId, new List() { new U2FDeviceAuthenticationRequest() { AppId = startedRegistration.AppId, Challenge = startedRegistration.Challenge, Version = global::U2F.Core.Crypto.U2F.U2FVersion, } }); return new ServerRegisterResponse { AppId = startedRegistration.AppId, Challenge = startedRegistration.Challenge, Version = startedRegistration.Version }; } public async Task CompleteRegistration(string userId, string deviceResponse, string name) { if (string.IsNullOrWhiteSpace(deviceResponse)) return false; if (!UserAuthenticationRequests.ContainsKey(userId) || !UserAuthenticationRequests[userId].Any()) { return false; } var registerResponse = RegisterResponse.FromJson(deviceResponse); //There is only 1 request when registering device var authenticationRequest = UserAuthenticationRequests[userId].First(); var startedRegistration = new StartedRegistration(authenticationRequest.Challenge, authenticationRequest.AppId); var registration = global::U2F.Core.Crypto.U2F.FinishRegistration(startedRegistration, registerResponse); UserAuthenticationRequests.AddOrReplace(userId, new List()); using (var context = _contextFactory.CreateContext()) { var duplicate = context.U2FDevices.Any(device => device.ApplicationUserId.Equals(userId, StringComparison.InvariantCulture) && device.KeyHandle.Equals(registration.KeyHandle) && device.PublicKey.Equals(registration.PublicKey)); if (duplicate) { throw new InvalidOperationException("The U2F Device has already been registered with this user"); } await context.U2FDevices.AddAsync(new U2FDevice() { AttestationCert = registration.AttestationCert, Counter = Convert.ToInt32(registration.Counter), Name = name, KeyHandle = registration.KeyHandle, PublicKey = registration.PublicKey, ApplicationUserId = userId }); await context.SaveChangesAsync(); } return true; } public async Task AuthenticateUser(string userId, string deviceResponse) { if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(deviceResponse)) return false; var authenticateResponse = AuthenticateResponse.FromJson(deviceResponse); using (var context = _contextFactory.CreateContext()) { var keyHandle = authenticateResponse.KeyHandle.Base64StringToByteArray(); var device = await context.U2FDevices.Where(fDevice => fDevice.ApplicationUserId.Equals(userId, StringComparison.InvariantCulture) && fDevice.KeyHandle.SequenceEqual(keyHandle)).SingleOrDefaultAsync(); if (device == null) return false; // User will have a authentication request for each device they have registered so get the one that matches the device key handle var authenticationRequest = UserAuthenticationRequests[userId].First(f => f.KeyHandle.Equals(authenticateResponse.KeyHandle, StringComparison.InvariantCulture)); var registration = new DeviceRegistration(device.KeyHandle, device.PublicKey, device.AttestationCert, Convert.ToUInt32(device.Counter)); var authentication = new StartedAuthentication(authenticationRequest.Challenge, authenticationRequest.AppId, authenticationRequest.KeyHandle); var challengeAuthenticationRequestMatch = UserAuthenticationRequests[userId].First(f => f.Challenge.Equals( authenticateResponse.GetClientData().Challenge, StringComparison.InvariantCulture)); if (authentication.Challenge != challengeAuthenticationRequestMatch.Challenge) { authentication = new StartedAuthentication(challengeAuthenticationRequestMatch.Challenge, authenticationRequest.AppId, authenticationRequest.KeyHandle); } global::U2F.Core.Crypto.U2F.FinishAuthentication(authentication, authenticateResponse, registration); UserAuthenticationRequests.AddOrReplace(userId, new List()); device.Counter = Convert.ToInt32(registration.Counter); await context.SaveChangesAsync(); } return true; } public async Task> GenerateDeviceChallenges(string userId, string appId) { using (var context = _contextFactory.CreateContext()) { var devices = await context.U2FDevices.Where(fDevice => fDevice.ApplicationUserId == userId).ToListAsync(); if (devices.Count == 0) return null; var requests = new List(); var serverChallenges = new List(); foreach (var registeredDevice in devices) { var challenge = global::U2F.Core.Crypto.U2F.StartAuthentication(appId, new DeviceRegistration(registeredDevice.KeyHandle, registeredDevice.PublicKey, registeredDevice.AttestationCert, (uint)registeredDevice.Counter)); serverChallenges.Add(new ServerChallenge() { challenge = challenge.Challenge, appId = challenge.AppId, version = challenge.Version, keyHandle = challenge.KeyHandle }); requests.Add( new U2FDeviceAuthenticationRequest() { AppId = appId, Challenge = challenge.Challenge, KeyHandle = registeredDevice.KeyHandle.ByteArrayToBase64String(), Version = global::U2F.Core.Crypto.U2F.U2FVersion }); } UserAuthenticationRequests.AddOrReplace(userId, requests); return serverChallenges; } } } }