btcpayserver/BTCPayServer/U2F/U2FService.cs

221 lines
8.7 KiB
C#

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.Services.U2F.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using U2F.Core.Models;
using U2F.Core.Utils;
namespace BTCPayServer.Services.U2F
{
public class U2FService
{
private readonly ApplicationDbContextFactory _contextFactory;
private ConcurrentDictionary<string, List<U2FDeviceAuthenticationRequest>> UserAuthenticationRequests
{
get;
set;
}
= new ConcurrentDictionary<string, List<U2FDeviceAuthenticationRequest>>();
public U2FService(ApplicationDbContextFactory contextFactory)
{
_contextFactory = contextFactory;
}
public async Task<List<U2FDevice>> GetDevices(string userId)
{
using (var context = _contextFactory.CreateContext())
{
return await context.U2FDevices
.Where(device => device.ApplicationUserId.Equals(userId, StringComparison.InvariantCulture))
.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<bool> HasDevices(string userId)
{
using (var context = _contextFactory.CreateContext())
{
return await context.U2FDevices.AnyAsync(fDevice => fDevice.ApplicationUserId.Equals(userId, StringComparison.InvariantCulture));
}
}
public ServerRegisterResponse StartDeviceRegistration(string userId, string appId)
{
var startedRegistration = global::U2F.Core.Crypto.U2F.StartRegistration(appId);
UserAuthenticationRequests.AddOrReplace(userId, new List<U2FDeviceAuthenticationRequest>()
{
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<bool> 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<RegisterResponse>(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<U2FDeviceAuthenticationRequest>());
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<bool> AuthenticateUser(string userId, string deviceResponse)
{
if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(deviceResponse))
return false;
var authenticateResponse =
AuthenticateResponse.FromJson<AuthenticateResponse>(deviceResponse);
using (var context = _contextFactory.CreateContext())
{
var device = await context.U2FDevices.SingleOrDefaultAsync(fDevice =>
fDevice.ApplicationUserId.Equals(userId, StringComparison.InvariantCulture) &&
fDevice.KeyHandle.SequenceEqual(authenticateResponse.KeyHandle.Base64StringToByteArray()));
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);
global::U2F.Core.Crypto.U2F.FinishAuthentication(authentication, authenticateResponse, registration);
UserAuthenticationRequests.AddOrReplace(userId, new List<U2FDeviceAuthenticationRequest>());
device.Counter = Convert.ToInt32(registration.Counter);
await context.SaveChangesAsync();
}
return true;
}
public async Task<List<ServerChallenge>> GenerateDeviceChallenges(string userId, string appId)
{
using (var context = _contextFactory.CreateContext())
{
var devices = await context.U2FDevices.Where(fDevice =>
fDevice.ApplicationUserId.Equals(userId, StringComparison.InvariantCulture)).ToListAsync();
if (devices.Count == 0)
return null;
var requests = new List<U2FDeviceAuthenticationRequest>();
var serverChallenges = new List<ServerChallenge>();
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;
}
}
}
}