Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d17ffd25ad |
@ -10,5 +10,16 @@ namespace BtcTransmuter.Abstractions.Extensions
|
||||
{
|
||||
return new HashSet<T>(source, comparer);
|
||||
}
|
||||
|
||||
public static void AddOrReplace<TKey, TValue>(
|
||||
this IDictionary<TKey, TValue> dico,
|
||||
TKey key,
|
||||
TValue value)
|
||||
{
|
||||
if (dico.ContainsKey(key))
|
||||
dico[key] = value;
|
||||
else
|
||||
dico.Add(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
BtcTransmuter.Abstractions/U2F/IU2FService.cs
Normal file
18
BtcTransmuter.Abstractions/U2F/IU2FService.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using BtcTransmuter.Data.Entities;
|
||||
using BtcTransmuter.Data.Entities.U2F;
|
||||
|
||||
namespace BtcTransmuter.Abstractions.U2F
|
||||
{
|
||||
public interface IU2FService
|
||||
{
|
||||
Task<List<U2FDevice>> GetDevices(string userId);
|
||||
Task RemoveDevice(string id, string userId);
|
||||
Task<bool> HasDevices(string userId);
|
||||
ServerRegisterResponse StartDeviceRegistration(string userId, string appId);
|
||||
Task<bool> CompleteRegistration(string userId, string deviceResponse, string name);
|
||||
Task<bool> AuthenticateUser(string userId, string deviceResponse);
|
||||
Task<List<ServerChallenge>> GenerateDeviceChallenges(string userId, string appId);
|
||||
}
|
||||
}
|
||||
10
BtcTransmuter.Data/Entities/U2F/ServerChallenge.cs
Normal file
10
BtcTransmuter.Data/Entities/U2F/ServerChallenge.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace BtcTransmuter.Data.Entities.U2F
|
||||
{
|
||||
public class ServerChallenge
|
||||
{
|
||||
public string challenge { get; set; }
|
||||
public string version { get; set; }
|
||||
public string appId { get; set; }
|
||||
public string keyHandle { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
namespace BtcTransmuter.Data.Entities.U2F
|
||||
{
|
||||
public class ServerRegisterResponse
|
||||
{
|
||||
public string AppId { get; set; }
|
||||
public string Challenge { get; set; }
|
||||
public string Version { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BtcTransmuter.Data.Entities.U2F
|
||||
{
|
||||
public class U2FDeviceAuthenticationRequest
|
||||
{
|
||||
public string KeyHandle { get; set; }
|
||||
|
||||
[Required] public string Challenge { get; set; }
|
||||
|
||||
[Required] [StringLength(200)] public string AppId { get; set; }
|
||||
|
||||
[Required] [StringLength(50)] public string Version { get; set; }
|
||||
}
|
||||
}
|
||||
22
BtcTransmuter.Data/Entities/U2FDevice.cs
Normal file
22
BtcTransmuter.Data/Entities/U2FDevice.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BtcTransmuter.Data.Entities
|
||||
{
|
||||
public class U2FDevice
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
[Required] public byte[] KeyHandle { get; set; }
|
||||
|
||||
[Required] public byte[] PublicKey { get; set; }
|
||||
|
||||
[Required] public byte[] AttestationCert { get; set; }
|
||||
|
||||
[Required] public int Counter { get; set; }
|
||||
|
||||
public string UserId { get; set; }
|
||||
public User User { get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace BtcTransmuter.Data.Entities
|
||||
@ -6,5 +6,6 @@ namespace BtcTransmuter.Data.Entities
|
||||
public class User : IdentityUser
|
||||
{
|
||||
public List<Recipe> Recipes { get; set; }
|
||||
public List<U2FDevice> U2FDevices { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +26,8 @@ namespace BtcTransmuter.Data
|
||||
public DbSet<RecipeInvocation> RecipeInvocations { get; set; }
|
||||
public DbSet<RecipeTrigger> RecipeTriggers { get; set; }
|
||||
public DbSet<RecipeAction> RecipeActions { get; set; }
|
||||
public DbSet<Settings> Settings { get; set; }
|
||||
public DbSet<Settings> Settings { get; set; }
|
||||
public DbSet<U2FDevice> U2FDevices { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
@ -78,4 +79,4 @@ namespace BtcTransmuter.Data
|
||||
return new ApplicationDbContext(optionsBuilder.Options, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,10 @@
|
||||
<OutputType>Library</OutputType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BtcTransmuter.Abstractions\BtcTransmuter.Abstractions.csproj"/>
|
||||
<ProjectReference Include="..\BtcTransmuter.Data\BtcTransmuter.Data.csproj"/>
|
||||
<ProjectReference Include="..\BtcTransmuter.Abstractions\BtcTransmuter.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\BtcTransmuter.Data\BtcTransmuter.Data.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="U2F.Core" Version="2.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@ -3,6 +3,7 @@ using BtcTransmuter.Abstractions.ExternalServices;
|
||||
using BtcTransmuter.Abstractions.Recipes;
|
||||
using BtcTransmuter.Abstractions.Settings;
|
||||
using BtcTransmuter.Abstractions.Triggers;
|
||||
using BtcTransmuter.Abstractions.U2F;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BtcTransmuter.Services
|
||||
@ -16,6 +17,7 @@ namespace BtcTransmuter.Services
|
||||
collection.AddSingleton<IActionDispatcher, ActionDispatcher>();
|
||||
collection.AddSingleton<ITriggerDispatcher, TriggerDispatcher>();
|
||||
collection.AddSingleton<ISettingsManager, SettingsManager>();
|
||||
collection.AddSingleton<IU2FService, U2FService>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
273
BtcTransmuter.Services/U2FService.cs
Normal file
273
BtcTransmuter.Services/U2FService.cs
Normal file
@ -0,0 +1,273 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BtcTransmuter.Abstractions.Extensions;
|
||||
using BtcTransmuter.Abstractions.U2F;
|
||||
using BtcTransmuter.Data;
|
||||
using BtcTransmuter.Data.Entities;
|
||||
using BtcTransmuter.Data.Entities.U2F;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using U2F.Core.Exceptions;
|
||||
using U2F.Core.Models;
|
||||
using U2F.Core.Utils;
|
||||
|
||||
namespace BtcTransmuter.Services
|
||||
{
|
||||
public class U2FService : IU2FService
|
||||
{
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
public U2FService(IServiceScopeFactory serviceScopeFactory)
|
||||
{
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
private ConcurrentDictionary<string, List<U2FDeviceAuthenticationRequest>> UserAuthenticationRequests
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
= new ConcurrentDictionary<string, List<U2FDeviceAuthenticationRequest>>();
|
||||
|
||||
public async Task<List<U2FDevice>> GetDevices(string userId)
|
||||
{
|
||||
using (var scope = _serviceScopeFactory.CreateScope())
|
||||
{
|
||||
using (var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>())
|
||||
{
|
||||
return await context.U2FDevices
|
||||
.Where(device => device.UserId == userId)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveDevice(string id, string userId)
|
||||
{
|
||||
using (var scope = _serviceScopeFactory.CreateScope())
|
||||
{
|
||||
using (var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>())
|
||||
{
|
||||
var device = await context.U2FDevices.FindAsync(id);
|
||||
if (device == null || !device.UserId.Equals(userId, StringComparison.InvariantCulture))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
context.U2FDevices.Remove(device);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> HasDevices(string userId)
|
||||
{
|
||||
using (var scope = _serviceScopeFactory.CreateScope())
|
||||
{
|
||||
using (var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>())
|
||||
{
|
||||
return await context.U2FDevices.Where(fDevice => fDevice.UserId == userId).AnyAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ServerRegisterResponse StartDeviceRegistration(string userId, string appId)
|
||||
{
|
||||
var startedRegistration = StartDeviceRegistrationCore(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 = FinishRegistrationCore(startedRegistration, registerResponse);
|
||||
|
||||
UserAuthenticationRequests.AddOrReplace(userId, new List<U2FDeviceAuthenticationRequest>());
|
||||
using (var scope = _serviceScopeFactory.CreateScope())
|
||||
{
|
||||
using (var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>())
|
||||
{
|
||||
var duplicate = context.U2FDevices.Any(device =>
|
||||
device.UserId == userId &&
|
||||
device.KeyHandle.Equals(registration.KeyHandle) &&
|
||||
device.PublicKey.Equals(registration.PublicKey));
|
||||
|
||||
if (duplicate)
|
||||
{
|
||||
throw new U2fException("The U2F Device has already been registered with this user");
|
||||
}
|
||||
|
||||
await context.U2FDevices.AddAsync(new U2FDevice()
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
AttestationCert = registration.AttestationCert,
|
||||
Counter = Convert.ToInt32(registration.Counter),
|
||||
Name = name,
|
||||
KeyHandle = registration.KeyHandle,
|
||||
PublicKey = registration.PublicKey,
|
||||
UserId = 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 scope = _serviceScopeFactory.CreateScope())
|
||||
{
|
||||
using (var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>())
|
||||
{
|
||||
var keyHandle = authenticateResponse.KeyHandle.Base64StringToByteArray();
|
||||
var device = await context.U2FDevices.Where(fDevice =>
|
||||
fDevice.UserId == userId &&
|
||||
fDevice.KeyHandle == 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);
|
||||
}
|
||||
|
||||
FinishAuthenticationCore(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 scope = _serviceScopeFactory.CreateScope())
|
||||
{
|
||||
using (var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>())
|
||||
{
|
||||
var devices = await context.U2FDevices.Where(fDevice => fDevice.UserId == userId)
|
||||
.ToListAsync();
|
||||
|
||||
if (devices.Count == 0)
|
||||
return null;
|
||||
|
||||
var requests = new List<U2FDeviceAuthenticationRequest>();
|
||||
|
||||
|
||||
var serverChallenges = new List<ServerChallenge>();
|
||||
foreach (var registeredDevice in devices)
|
||||
{
|
||||
var challenge = StartAuthenticationCore(appId, registeredDevice);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual StartedRegistration StartDeviceRegistrationCore(string appId)
|
||||
{
|
||||
return global::U2F.Core.Crypto.U2F.StartRegistration(appId);
|
||||
}
|
||||
|
||||
protected virtual DeviceRegistration FinishRegistrationCore(StartedRegistration startedRegistration,
|
||||
RegisterResponse registerResponse)
|
||||
{
|
||||
return global::U2F.Core.Crypto.U2F.FinishRegistration(startedRegistration, registerResponse);
|
||||
}
|
||||
|
||||
protected virtual StartedAuthentication StartAuthenticationCore(string appId, U2FDevice registeredDevice)
|
||||
{
|
||||
return global::U2F.Core.Crypto.U2F.StartAuthentication(appId,
|
||||
new DeviceRegistration(registeredDevice.KeyHandle, registeredDevice.PublicKey,
|
||||
registeredDevice.AttestationCert, (uint) registeredDevice.Counter));
|
||||
}
|
||||
|
||||
protected virtual void FinishAuthenticationCore(StartedAuthentication authentication,
|
||||
AuthenticateResponse authenticateResponse, DeviceRegistration registration)
|
||||
{
|
||||
global::U2F.Core.Crypto.U2F.FinishAuthentication(authentication, authenticateResponse, registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
BtcTransmuter/Models/U2F/AddU2FDeviceViewModel.cs
Normal file
12
BtcTransmuter/Models/U2F/AddU2FDeviceViewModel.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace BtcTransmuter.Models.U2F
|
||||
{
|
||||
public class AddU2FDeviceViewModel
|
||||
{
|
||||
public string AppId{ get; set; }
|
||||
public string Challenge { get; set; }
|
||||
public string Version { get; set; }
|
||||
public string DeviceResponse { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
30
BtcTransmuter/Models/U2F/LoginWithU2FViewModel.cs
Normal file
30
BtcTransmuter/Models/U2F/LoginWithU2FViewModel.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BtcTransmuter.Data.Entities.U2F;
|
||||
|
||||
namespace BtcTransmuter.Models.U2F
|
||||
{
|
||||
public class LoginWithU2FViewModel
|
||||
{
|
||||
public string UserId { get; set; }
|
||||
[Required]
|
||||
[Display(Name = "App id")]
|
||||
public string AppId { get; set; }
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Version")]
|
||||
public string Version { get; set; }
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Device Response")]
|
||||
public string DeviceResponse { get; set; }
|
||||
|
||||
[Display(Name = "Challenges")]
|
||||
public List<ServerChallenge> Challenges { get; set; }
|
||||
|
||||
[Display(Name = "Challenge")]
|
||||
public string Challenge { get; set; }
|
||||
|
||||
public bool RememberMe { get; set; }
|
||||
}
|
||||
}
|
||||
10
BtcTransmuter/Models/U2F/U2FAuthenticationViewModel.cs
Normal file
10
BtcTransmuter/Models/U2F/U2FAuthenticationViewModel.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using BtcTransmuter.Data.Entities;
|
||||
|
||||
namespace BtcTransmuter.Models.U2F
|
||||
{
|
||||
public class U2FAuthenticationViewModel
|
||||
{
|
||||
public List<U2FDevice> Devices { get; set; }
|
||||
}
|
||||
}
|
||||
@ -44,6 +44,7 @@ namespace BtcTransmuter
|
||||
identityOptions.Password.RequireUppercase = false;
|
||||
identityOptions.Password.RequiredUniqueChars = 0;
|
||||
identityOptions.Password.RequireNonAlphanumeric = false;
|
||||
identityOptions.User.RequireUniqueEmail = true;
|
||||
});
|
||||
|
||||
services.Configure<SecurityStampValidatorOptions>(validatorOptions => validatorOptions.ValidationInterval = TimeSpan.FromSeconds(50));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user