Compare commits

...

1 Commits
master ... u2f

Author SHA1 Message Date
Kukks
d17ffd25ad x 2020-02-27 10:22:15 +01:00
15 changed files with 426 additions and 8 deletions

View File

@ -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);
}
}
}
}

View 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);
}
}

View 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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View 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; }
}
}

View File

@ -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; }
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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>

View File

@ -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>();
}
}
}
}

View 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);
}
}
}

View 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; }
}
}

View 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; }
}
}

View 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; }
}
}

View File

@ -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));