feature: online session authentication and one-way decryption
This commit is contained in:
@@ -484,6 +484,7 @@
|
|||||||
<Compile Include="Interop\ActiveDirectory\IADObject.cs" />
|
<Compile Include="Interop\ActiveDirectory\IADObject.cs" />
|
||||||
<Compile Include="Interop\DiscoServices\ActivationCleanupTask.cs" />
|
<Compile Include="Interop\DiscoServices\ActivationCleanupTask.cs" />
|
||||||
<Compile Include="Interop\DiscoServices\ActivationService.cs" />
|
<Compile Include="Interop\DiscoServices\ActivationService.cs" />
|
||||||
|
<Compile Include="Interop\DiscoServices\AuthenticationSessionScope.cs" />
|
||||||
<Compile Include="Interop\DiscoServices\DiscoServiceHelpers.cs" />
|
<Compile Include="Interop\DiscoServices\DiscoServiceHelpers.cs" />
|
||||||
<Compile Include="Interop\DiscoServices\Jobs.cs" />
|
<Compile Include="Interop\DiscoServices\Jobs.cs" />
|
||||||
<Compile Include="Interop\DiscoServices\LicenseValidationTask.cs" />
|
<Compile Include="Interop\DiscoServices\LicenseValidationTask.cs" />
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ namespace Disco.Services.Interop.DiscoServices
|
|||||||
{
|
{
|
||||||
public class ActivationService
|
public class ActivationService
|
||||||
{
|
{
|
||||||
|
private static readonly HttpClient httpClient;
|
||||||
private static readonly byte[] onlineServicesActivationKey;
|
private static readonly byte[] onlineServicesActivationKey;
|
||||||
private readonly DiscoDataContext database;
|
private readonly DiscoDataContext database;
|
||||||
|
|
||||||
@@ -29,6 +30,10 @@ namespace Disco.Services.Interop.DiscoServices
|
|||||||
resourceStream.Read(key, 0, key.Length);
|
resourceStream.Read(key, 0, key.Length);
|
||||||
onlineServicesActivationKey = key;
|
onlineServicesActivationKey = key;
|
||||||
}
|
}
|
||||||
|
httpClient = new HttpClient(new OnlineServicesAuthenticatedHandler())
|
||||||
|
{
|
||||||
|
BaseAddress = DiscoServiceHelpers.ActivationServiceUrl
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public ActivationService(DiscoDataContext database)
|
public ActivationService(DiscoDataContext database)
|
||||||
@@ -247,6 +252,38 @@ namespace Disco.Services.Interop.DiscoServices
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<T> Post<T>(string url, object request)
|
||||||
|
{
|
||||||
|
var stream = new MemoryStream();
|
||||||
|
using (var jsonWriter = new StreamWriter(stream, new UTF8Encoding(false), 1024, true))
|
||||||
|
{
|
||||||
|
var serializer = new JsonSerializer();
|
||||||
|
serializer.Serialize(jsonWriter, request);
|
||||||
|
}
|
||||||
|
stream.Position = 0;
|
||||||
|
using (var content = new StreamContent(stream))
|
||||||
|
{
|
||||||
|
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||||
|
|
||||||
|
using (var response = await httpClient.PostAsync(url, content))
|
||||||
|
{
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
using (var responseContent = await response.Content.ReadAsStreamAsync())
|
||||||
|
{
|
||||||
|
using (var reader = new StreamReader(responseContent, Encoding.UTF8))
|
||||||
|
{
|
||||||
|
using (var jsonReader = new JsonTextReader(reader))
|
||||||
|
{
|
||||||
|
var serializer = new JsonSerializer();
|
||||||
|
return serializer.Deserialize<T>(jsonReader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static bool ValidateSignature(ChallengeResponse response)
|
private static bool ValidateSignature(ChallengeResponse response)
|
||||||
{
|
{
|
||||||
var stream = new MemoryStream();
|
var stream = new MemoryStream();
|
||||||
@@ -255,7 +292,7 @@ namespace Disco.Services.Interop.DiscoServices
|
|||||||
stream.Write(response.Challenge, 0, response.Challenge.Length);
|
stream.Write(response.Challenge, 0, response.Challenge.Length);
|
||||||
stream.Write(response.ChallengeIv, 0, response.ChallengeIv.Length);
|
stream.Write(response.ChallengeIv, 0, response.ChallengeIv.Length);
|
||||||
|
|
||||||
return ValidateSignature(stream.ToArray(), response.Signature);
|
return ValidateOnlineServicesSignature(stream.ToArray(), response.Signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ValidateSignature(Guid activationId, byte[] challenge, byte[] challengeIv, byte[] signature)
|
private static bool ValidateSignature(Guid activationId, byte[] challenge, byte[] challengeIv, byte[] signature)
|
||||||
@@ -264,10 +301,10 @@ namespace Disco.Services.Interop.DiscoServices
|
|||||||
stream.Write(activationId.ToByteArray(), 0, 16);
|
stream.Write(activationId.ToByteArray(), 0, 16);
|
||||||
stream.Write(challenge, 0, challenge.Length);
|
stream.Write(challenge, 0, challenge.Length);
|
||||||
stream.Write(challengeIv, 0, challengeIv.Length);
|
stream.Write(challengeIv, 0, challengeIv.Length);
|
||||||
return ValidateSignature(stream.ToArray(), signature);
|
return ValidateOnlineServicesSignature(stream.ToArray(), signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ValidateSignature(byte[] bytes, byte[] signature)
|
public static bool ValidateOnlineServicesSignature(byte[] bytes, byte[] signature)
|
||||||
{
|
{
|
||||||
byte[] hash;
|
byte[] hash;
|
||||||
using (var hasher = SHA256.Create())
|
using (var hasher = SHA256.Create())
|
||||||
@@ -313,6 +350,11 @@ namespace Disco.Services.Interop.DiscoServices
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static byte[] SignSHA256Hash(byte[] hash)
|
||||||
|
{
|
||||||
|
return SignHash(OnlineServicesAuthentication.Key, hash);
|
||||||
|
}
|
||||||
|
|
||||||
private static byte[] Encrypt(byte[] privateKey, byte[] iv, long timeStamp, byte[] data)
|
private static byte[] Encrypt(byte[] privateKey, byte[] iv, long timeStamp, byte[] data)
|
||||||
{
|
{
|
||||||
var key = DeriveEncryptionKey(privateKey, iv, timeStamp);
|
var key = DeriveEncryptionKey(privateKey, iv, timeStamp);
|
||||||
@@ -336,6 +378,17 @@ namespace Disco.Services.Interop.DiscoServices
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static byte[] Encrypt(byte[] data, out byte[] iv, out long timeStamp)
|
||||||
|
{
|
||||||
|
iv = new byte[16];
|
||||||
|
using (var rng = RandomNumberGenerator.Create())
|
||||||
|
rng.GetBytes(iv);
|
||||||
|
|
||||||
|
timeStamp = DateTime.UtcNow.Ticks;
|
||||||
|
|
||||||
|
return Encrypt(OnlineServicesAuthentication.Key, iv, timeStamp, data);
|
||||||
|
}
|
||||||
|
|
||||||
private static byte[] Decrypt(byte[] privateKey, byte[] iv, long timeStamp, byte[] data)
|
private static byte[] Decrypt(byte[] privateKey, byte[] iv, long timeStamp, byte[] data)
|
||||||
{
|
{
|
||||||
var key = DeriveEncryptionKey(privateKey, iv, timeStamp);
|
var key = DeriveEncryptionKey(privateKey, iv, timeStamp);
|
||||||
@@ -360,6 +413,63 @@ namespace Disco.Services.Interop.DiscoServices
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static byte[] Decrypt(byte[] data, byte[] iv, long timeStamp)
|
||||||
|
{
|
||||||
|
return Decrypt(OnlineServicesAuthentication.Key, iv, timeStamp, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] OneWayDecrypt(byte[] data)
|
||||||
|
{
|
||||||
|
var span = data.AsSpan();
|
||||||
|
|
||||||
|
if (span.Length < 13)
|
||||||
|
throw new ArgumentException("Data is too short", nameof(data));
|
||||||
|
Span<byte> magicBytes = new byte[] { 0xF0, 0x9F, 0x94, 0x8F, 0x44, 0x69, 0x73, 0x63, 0x6F, 0x49, 0x43, 0x54 }.AsSpan();
|
||||||
|
if (!MemoryExtensions.SequenceEqual(magicBytes, span.Slice(0, 12)))
|
||||||
|
throw new InvalidOperationException("Invalid format signature");
|
||||||
|
|
||||||
|
span = span.Slice(12);
|
||||||
|
|
||||||
|
byte[] readChunk(ref Span<byte> source)
|
||||||
|
{
|
||||||
|
var length = source[0];
|
||||||
|
if (source.Length < (length + 1))
|
||||||
|
throw new ArgumentException("Data is too short", nameof(data));
|
||||||
|
var buffer = new byte[length];
|
||||||
|
source.Slice(1, length).CopyTo(buffer);
|
||||||
|
source = source.Slice(length + 1);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
var publicKey = readChunk(ref span);
|
||||||
|
var iv = readChunk(ref span);
|
||||||
|
var signature = readChunk(ref span);
|
||||||
|
|
||||||
|
var key = DeriveOneWayDecryptingKey(OnlineServicesAuthentication.Key, publicKey, iv);
|
||||||
|
|
||||||
|
var outputStream = new MemoryStream();
|
||||||
|
using (var aes = Aes.Create())
|
||||||
|
{
|
||||||
|
aes.Key = key;
|
||||||
|
aes.IV = iv;
|
||||||
|
aes.Mode = CipherMode.CBC;
|
||||||
|
aes.Padding = PaddingMode.PKCS7;
|
||||||
|
|
||||||
|
using (var decryptor = aes.CreateDecryptor())
|
||||||
|
{
|
||||||
|
var ms = new MemoryStream(span.ToArray());
|
||||||
|
using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
|
||||||
|
{
|
||||||
|
cs.CopyTo(outputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var output = outputStream.ToArray();
|
||||||
|
if (!ValidateOnlineServicesSignature(output, signature))
|
||||||
|
throw new InvalidOperationException("Invalid encrypted data signature");
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
private static byte[] DeriveEncryptionKey(byte[] privateKey, byte[] iv, long timeStamp)
|
private static byte[] DeriveEncryptionKey(byte[] privateKey, byte[] iv, long timeStamp)
|
||||||
{
|
{
|
||||||
using (var serverKey = CngKey.Import(onlineServicesActivationKey, CngKeyBlobFormat.EccPublicBlob))
|
using (var serverKey = CngKey.Import(onlineServicesActivationKey, CngKeyBlobFormat.EccPublicBlob))
|
||||||
@@ -377,6 +487,23 @@ namespace Disco.Services.Interop.DiscoServices
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static byte[] DeriveOneWayDecryptingKey(byte[] privateKey, byte[] publicKey, byte[] iv)
|
||||||
|
{
|
||||||
|
using (var ephemeralKey = CngKey.Import(publicKey, CngKeyBlobFormat.EccPublicBlob))
|
||||||
|
{
|
||||||
|
using (var clientKey = CngKey.Import(privateKey, CngKeyBlobFormat.EccPrivateBlob))
|
||||||
|
{
|
||||||
|
using (var ephemeralEcdh = new ECDiffieHellmanCng(ephemeralKey))
|
||||||
|
{
|
||||||
|
using (var ecdh = new ECDiffieHellmanCng(clientKey))
|
||||||
|
{
|
||||||
|
return ecdh.DeriveKeyFromHash(ephemeralEcdh.PublicKey, HashAlgorithmName.SHA256, iv, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static (byte[] privateKey, byte[] publicKey) GenerateActivationKey()
|
private static (byte[] privateKey, byte[] publicKey) GenerateActivationKey()
|
||||||
{
|
{
|
||||||
using (var key = CngKey.Create(CngAlgorithm.ECDiffieHellmanP521, null, new CngKeyCreationParameters
|
using (var key = CngKey.Create(CngAlgorithm.ECDiffieHellmanP521, null, new CngKeyCreationParameters
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Disco.Services.Interop.DiscoServices
|
||||||
|
{
|
||||||
|
[Flags]
|
||||||
|
public enum AuthenticationSessionScope
|
||||||
|
{
|
||||||
|
Ping = 1,
|
||||||
|
Host = 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
using Disco.Data.Repository;
|
using Disco.Data.Repository;
|
||||||
|
using Disco.Models.Repository;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
@@ -14,7 +16,7 @@ namespace Disco.Services.Interop.DiscoServices
|
|||||||
{
|
{
|
||||||
public static class OnlineServicesAuthentication
|
public static class OnlineServicesAuthentication
|
||||||
{
|
{
|
||||||
private static SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
|
private static readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
|
||||||
private static readonly Guid deploymentId;
|
private static readonly Guid deploymentId;
|
||||||
private static Guid? activationId;
|
private static Guid? activationId;
|
||||||
private static byte[] key;
|
private static byte[] key;
|
||||||
@@ -31,11 +33,12 @@ namespace Disco.Services.Interop.DiscoServices
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsActivated => activationId.HasValue;
|
public static bool IsActivated => activationId.HasValue;
|
||||||
|
internal static byte[] Key => key.ToArray() ?? throw new InvalidOperationException("Not activated");
|
||||||
|
|
||||||
public static string GetToken()
|
public static string GetToken()
|
||||||
=> GetTokenAsync().Result;
|
=> GetTokenAsync().Result;
|
||||||
|
|
||||||
public async static Task<string> GetTokenAsync()
|
public static async Task<string> GetTokenAsync()
|
||||||
{
|
{
|
||||||
var localExpires = tokenExpires;
|
var localExpires = tokenExpires;
|
||||||
var localToken = token;
|
var localToken = token;
|
||||||
@@ -56,11 +59,6 @@ namespace Disco.Services.Interop.DiscoServices
|
|||||||
if (!IsActivated)
|
if (!IsActivated)
|
||||||
throw new InvalidOperationException("Not activated");
|
throw new InvalidOperationException("Not activated");
|
||||||
|
|
||||||
using (var httpClient = new HttpClient())
|
|
||||||
{
|
|
||||||
httpClient.BaseAddress = DiscoServiceHelpers.ActivationServiceUrl;
|
|
||||||
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
|
||||||
|
|
||||||
var timeStamp = DateTime.UtcNow.ToUnixEpoc();
|
var timeStamp = DateTime.UtcNow.ToUnixEpoc();
|
||||||
var iv = new byte[32];
|
var iv = new byte[32];
|
||||||
using (var rng = RandomNumberGenerator.Create())
|
using (var rng = RandomNumberGenerator.Create())
|
||||||
@@ -91,6 +89,11 @@ namespace Disco.Services.Interop.DiscoServices
|
|||||||
{
|
{
|
||||||
request.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
request.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||||
|
|
||||||
|
using (var httpClient = new HttpClient())
|
||||||
|
{
|
||||||
|
httpClient.BaseAddress = DiscoServiceHelpers.ActivationServiceUrl;
|
||||||
|
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
|
||||||
var response = await httpClient.PostAsync("/api/authenticate", request);
|
var response = await httpClient.PostAsync("/api/authenticate", request);
|
||||||
|
|
||||||
if (response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.BadRequest)
|
if (response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.BadRequest)
|
||||||
@@ -120,6 +123,32 @@ namespace Disco.Services.Interop.DiscoServices
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<Uri> CreateSession(AuthenticationSessionScope scope, User user, Uri returnUrl)
|
||||||
|
{
|
||||||
|
if (!IsActivated)
|
||||||
|
throw new InvalidOperationException("Not activated");
|
||||||
|
|
||||||
|
var request = new AuthenticationSessionRequest()
|
||||||
|
{
|
||||||
|
DeploymentId = deploymentId,
|
||||||
|
ActivationId = activationId.Value,
|
||||||
|
TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||||
|
Scope = scope,
|
||||||
|
UserId = user.UserId,
|
||||||
|
UserName = user.DisplayName ?? string.Empty,
|
||||||
|
UserEmail = user.EmailAddress,
|
||||||
|
UserPhone = user.PhoneNumber,
|
||||||
|
ReturnUrl = returnUrl.ToString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await ActivationService.Post<AuthenticationSessionResponse>($"/api/authenticate/session", request);
|
||||||
|
|
||||||
|
if (response.Success)
|
||||||
|
return new Uri(response.Endpoint);
|
||||||
|
else
|
||||||
|
throw new InvalidOperationException($"Failed to create authentication session: {response.ErrorMessage}");
|
||||||
|
}
|
||||||
|
|
||||||
internal static void UpdateActivation(DiscoDataContext database)
|
internal static void UpdateActivation(DiscoDataContext database)
|
||||||
{
|
{
|
||||||
semaphore.Wait();
|
semaphore.Wait();
|
||||||
@@ -165,5 +194,25 @@ namespace Disco.Services.Interop.DiscoServices
|
|||||||
public int? ExpiresInSeconds { get; set; }
|
public int? ExpiresInSeconds { get; set; }
|
||||||
public string ErrorMessage { get; set; }
|
public string ErrorMessage { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class AuthenticationSessionRequest
|
||||||
|
{
|
||||||
|
public Guid DeploymentId { get; set; }
|
||||||
|
public Guid ActivationId { get; set; }
|
||||||
|
public long TimeStamp { get; set; }
|
||||||
|
public AuthenticationSessionScope Scope { get; set; }
|
||||||
|
public string UserId { get; set; }
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public string UserEmail { get; set; }
|
||||||
|
public string UserPhone { get; set; }
|
||||||
|
public string ReturnUrl { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AuthenticationSessionResponse
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Endpoint { get; set; }
|
||||||
|
public string ErrorMessage { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user