diff --git a/Disco.Services/Disco.Services.csproj b/Disco.Services/Disco.Services.csproj
index c94f9a01..1b57ab56 100644
--- a/Disco.Services/Disco.Services.csproj
+++ b/Disco.Services/Disco.Services.csproj
@@ -484,6 +484,7 @@
+
diff --git a/Disco.Services/Interop/DiscoServices/ActivationService.cs b/Disco.Services/Interop/DiscoServices/ActivationService.cs
index cfcda7ab..d9cb1b12 100644
--- a/Disco.Services/Interop/DiscoServices/ActivationService.cs
+++ b/Disco.Services/Interop/DiscoServices/ActivationService.cs
@@ -18,6 +18,7 @@ namespace Disco.Services.Interop.DiscoServices
{
public class ActivationService
{
+ private static readonly HttpClient httpClient;
private static readonly byte[] onlineServicesActivationKey;
private readonly DiscoDataContext database;
@@ -29,6 +30,10 @@ namespace Disco.Services.Interop.DiscoServices
resourceStream.Read(key, 0, key.Length);
onlineServicesActivationKey = key;
}
+ httpClient = new HttpClient(new OnlineServicesAuthenticatedHandler())
+ {
+ BaseAddress = DiscoServiceHelpers.ActivationServiceUrl
+ };
}
public ActivationService(DiscoDataContext database)
@@ -247,6 +252,38 @@ namespace Disco.Services.Interop.DiscoServices
}
}
+ public static async Task Post(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(jsonReader);
+ }
+ }
+ }
+ }
+ }
+ }
+
private static bool ValidateSignature(ChallengeResponse response)
{
var stream = new MemoryStream();
@@ -255,7 +292,7 @@ namespace Disco.Services.Interop.DiscoServices
stream.Write(response.Challenge, 0, response.Challenge.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)
@@ -264,10 +301,10 @@ namespace Disco.Services.Interop.DiscoServices
stream.Write(activationId.ToByteArray(), 0, 16);
stream.Write(challenge, 0, challenge.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;
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)
{
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)
{
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 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 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)
{
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()
{
using (var key = CngKey.Create(CngAlgorithm.ECDiffieHellmanP521, null, new CngKeyCreationParameters
diff --git a/Disco.Services/Interop/DiscoServices/AuthenticationSessionScope.cs b/Disco.Services/Interop/DiscoServices/AuthenticationSessionScope.cs
new file mode 100644
index 00000000..d02154f7
--- /dev/null
+++ b/Disco.Services/Interop/DiscoServices/AuthenticationSessionScope.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Disco.Services.Interop.DiscoServices
+{
+ [Flags]
+ public enum AuthenticationSessionScope
+ {
+ Ping = 1,
+ Host = 2,
+ }
+}
diff --git a/Disco.Services/Interop/DiscoServices/OnlineServicesAuthentication.cs b/Disco.Services/Interop/DiscoServices/OnlineServicesAuthentication.cs
index bbe99ebb..3319385b 100644
--- a/Disco.Services/Interop/DiscoServices/OnlineServicesAuthentication.cs
+++ b/Disco.Services/Interop/DiscoServices/OnlineServicesAuthentication.cs
@@ -1,7 +1,9 @@
using Disco.Data.Repository;
+using Disco.Models.Repository;
using Newtonsoft.Json;
using System;
using System.IO;
+using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
@@ -14,7 +16,7 @@ namespace Disco.Services.Interop.DiscoServices
{
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 Guid? activationId;
private static byte[] key;
@@ -31,11 +33,12 @@ namespace Disco.Services.Interop.DiscoServices
}
public static bool IsActivated => activationId.HasValue;
+ internal static byte[] Key => key.ToArray() ?? throw new InvalidOperationException("Not activated");
public static string GetToken()
=> GetTokenAsync().Result;
- public async static Task GetTokenAsync()
+ public static async Task GetTokenAsync()
{
var localExpires = tokenExpires;
var localToken = token;
@@ -56,40 +59,40 @@ namespace Disco.Services.Interop.DiscoServices
if (!IsActivated)
throw new InvalidOperationException("Not activated");
- using (var httpClient = new HttpClient())
+ var timeStamp = DateTime.UtcNow.ToUnixEpoc();
+ var iv = new byte[32];
+ using (var rng = RandomNumberGenerator.Create())
+ rng.GetBytes(iv);
+
+ var dataStream = new MemoryStream(16 + 16 + 8 + iv.Length);
+ dataStream.Write(deploymentId.ToByteArray(), 0, 16);
+ dataStream.Write(activationId.Value.ToByteArray(), 0, 16);
+ dataStream.Write(BitConverter.GetBytes(timeStamp), 0, 8);
+ dataStream.Write(iv, 0, iv.Length);
+ byte[] hash;
+ using (var hasher = SHA256.Create())
+ hash = hasher.ComputeHash(dataStream.ToArray());
+
+ var signature = ActivationService.SignHash(key, hash);
+
+ var body = new AuthenticationRequest()
{
- httpClient.BaseAddress = DiscoServiceHelpers.ActivationServiceUrl;
- httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+ DeploymentId = deploymentId,
+ ActivationId = activationId.Value,
+ TimeStamp = timeStamp,
+ IV = iv,
+ Signature = signature,
+ };
+ var requestJson = JsonConvert.SerializeObject(body);
- var timeStamp = DateTime.UtcNow.ToUnixEpoc();
- var iv = new byte[32];
- using (var rng = RandomNumberGenerator.Create())
- rng.GetBytes(iv);
+ using (var request = new ByteArrayContent(Encoding.UTF8.GetBytes(requestJson)))
+ {
+ request.Headers.ContentType = new MediaTypeHeaderValue("application/json");
- var dataStream = new MemoryStream(16 + 16 + 8 + iv.Length);
- dataStream.Write(deploymentId.ToByteArray(), 0, 16);
- dataStream.Write(activationId.Value.ToByteArray(), 0, 16);
- dataStream.Write(BitConverter.GetBytes(timeStamp), 0, 8);
- dataStream.Write(iv, 0, iv.Length);
- byte[] hash;
- using (var hasher = SHA256.Create())
- hash = hasher.ComputeHash(dataStream.ToArray());
-
- var signature = ActivationService.SignHash(key, hash);
-
- var body = new AuthenticationRequest()
+ using (var httpClient = new HttpClient())
{
- DeploymentId = deploymentId,
- ActivationId = activationId.Value,
- TimeStamp = timeStamp,
- IV = iv,
- Signature = signature,
- };
- var requestJson = JsonConvert.SerializeObject(body);
-
- using (var request = new ByteArrayContent(Encoding.UTF8.GetBytes(requestJson)))
- {
- request.Headers.ContentType = new MediaTypeHeaderValue("application/json");
+ httpClient.BaseAddress = DiscoServiceHelpers.ActivationServiceUrl;
+ httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var response = await httpClient.PostAsync("/api/authenticate", request);
@@ -120,6 +123,32 @@ namespace Disco.Services.Interop.DiscoServices
}
}
+ public static async Task 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($"/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)
{
semaphore.Wait();
@@ -165,5 +194,25 @@ namespace Disco.Services.Interop.DiscoServices
public int? ExpiresInSeconds { 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; }
+ }
}
}