diff --git a/.idea/.idea.Abyss/.idea/workspace.xml b/.idea/.idea.Abyss/.idea/workspace.xml index 5a018f1..f885b75 100644 --- a/.idea/.idea.Abyss/.idea/workspace.xml +++ b/.idea/.idea.Abyss/.idea/workspace.xml @@ -11,6 +11,9 @@ + + + diff --git a/Abyss/Components/Services/AbyssService.cs b/Abyss/Components/Services/AbyssService.cs index 2107fcb..cfae61e 100644 --- a/Abyss/Components/Services/AbyssService.cs +++ b/Abyss/Components/Services/AbyssService.cs @@ -5,7 +5,7 @@ using Abyss.Components.Tools; namespace Abyss.Components.Services; -public class AbyssService(ILogger logger, ConfigureService config) : IHostedService, IDisposable +public class AbyssService(ILogger logger, ConfigureService config, UserService user) : IHostedService, IDisposable { private Task? _executingTask; private CancellationTokenSource? _cts; @@ -52,10 +52,9 @@ public class AbyssService(ILogger logger, ConfigureService config) private async Task ClientHandlerAsync(TcpClient client, CancellationToken cancellationToken) { - var stream = await client.GetAbyssStreamAsync(ct: cancellationToken); - try { + await using var stream = await client.GetAbyssStreamAsync(ct: cancellationToken, us: user); var request = HttpHelper.Parse(await HttpReader.ReadHttpMessageAsync(stream, cancellationToken)); var port = 80; var sp = request.RequestUri?.ToString().Split(':') ?? []; @@ -137,7 +136,6 @@ public class AbyssService(ILogger logger, ConfigureService config) } finally { - stream.Close(); client.Close(); client.Dispose(); } diff --git a/Abyss/Components/Services/UserService.cs b/Abyss/Components/Services/UserService.cs index 946d196..8050c32 100644 --- a/Abyss/Components/Services/UserService.cs +++ b/Abyss/Components/Services/UserService.cs @@ -174,11 +174,38 @@ public class UserService } } - static bool VerifySignature(PublicKey publicKey, byte[] data, byte[] signature) + public static bool VerifySignature(PublicKey publicKey, byte[] data, byte[] signature) { var algorithm = SignatureAlgorithm.Ed25519; return algorithm.Verify(publicKey, data, signature); } + + public async Task VerifyAny(byte[] data, byte[] signature) + { + var users = await _database.Table().ToListAsync(); + foreach (var u in users) + { + try + { + var pubKeyBytes = Convert.FromBase64String(u.PublicKey); + var pubKey = PublicKey.Import( + SignatureAlgorithm.Ed25519, + pubKeyBytes, + KeyBlobFormat.RawPublicKey); + + if (VerifySignature(pubKey, data, signature)) + { + _logger.LogInformation($"Signature verified using user {u.Name}"); + return true; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, $"Failed to import public key for {u.Name}"); + } + } + return false; + } public string CreateToken(string user, string ip, TimeSpan lifetime) { diff --git a/Abyss/Components/Tools/AbyssStream.cs b/Abyss/Components/Tools/AbyssStream.cs index 566d015..347b927 100644 --- a/Abyss/Components/Tools/AbyssStream.cs +++ b/Abyss/Components/Tools/AbyssStream.cs @@ -9,15 +9,17 @@ using System.Data; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Security.Cryptography; - +using System.Text; +using Abyss.Components.Services; +using Microsoft.AspNetCore.Authentication; using NSec.Cryptography; using ChaCha20Poly1305 = System.Security.Cryptography.ChaCha20Poly1305; namespace Abyss.Components.Tools { - // TODO: Since C25519 has already been used for user authentication, - // TODO: why not use that public key to verify user identity when establishing a secure channel here? + // TODO: (complete) Since C25519 has already been used for user authentication, + // TODO: (complete) why not use that public key to verify user identity when establishing a secure channel here? public sealed class AbyssStream : NetworkStream, IDisposable { private const int PublicKeyLength = 32; @@ -63,7 +65,7 @@ namespace Abyss.Components.Tools /// Handshake: X25519 public exchange (raw) -> shared secret -> HKDF -> AEAD key + saltA + saltB /// send/recv salts are assigned deterministically by lexicographic comparison of raw public keys. /// - public static async Task CreateAsync(TcpClient client, byte[]? privateKeyRaw = null, CancellationToken cancellationToken = default) + public static async Task CreateAsync(TcpClient client, UserService us, byte[]? privateKeyRaw = null, CancellationToken cancellationToken = default) { if (client == null) throw new ArgumentNullException(nameof(client)); var socket = client.Client ?? throw new ArgumentException("TcpClient has no underlying socket"); @@ -104,6 +106,27 @@ namespace Abyss.Components.Tools await ReadExactFromSocketAsync(socket, remotePublic, 0, PublicKeyLength, cancellationToken).ConfigureAwait(false); + var ch = Encoding.UTF8.GetBytes(UserService.GenerateRandomAsciiString(32)); + sent = 0; + while (sent < ch.Length) + { + var toSend = new ReadOnlyMemory(ch, sent, ch.Length - sent); + sent += await socket.SendAsync(toSend, SocketFlags.None, cancellationToken).ConfigureAwait(false); + } + + var rch = new byte[64]; + await ReadExactFromSocketAsync(socket, rch, 0, 64, cancellationToken).ConfigureAwait(false); + bool rau = await us.VerifyAny(ch, rch); + if (!rau) throw new AuthenticationFailureException(""); + + var ack = Encoding.UTF8.GetBytes(UserService.GenerateRandomAsciiString(16)); + sent = 0; + while (sent < ack.Length) + { + var toSend = new ReadOnlyMemory(ack, sent, ack.Length - sent); + sent += await socket.SendAsync(toSend, SocketFlags.None, cancellationToken).ConfigureAwait(false); + } + // 3) Compute shared secret (X25519) PublicKey remotePub; try @@ -520,7 +543,7 @@ namespace Abyss.Components.Tools public static class TcpClientAbyssExtensions { - public static Task GetAbyssStreamAsync(this TcpClient client, byte[]? privateKeyRaw = null, CancellationToken ct = default) - => AbyssStream.CreateAsync(client, privateKeyRaw, ct); + public static Task GetAbyssStreamAsync(this TcpClient client, UserService us, byte[]? privateKeyRaw = null, CancellationToken ct = default) + => AbyssStream.CreateAsync(client, us, privateKeyRaw, ct); } }