From 5b8afd5c1d61513c2fa38d779cd0956842fb6384 Mon Sep 17 00:00:00 2001 From: Skye <22365940+Skyedra@users.noreply.github.com> Date: Sat, 10 Aug 2024 23:12:38 -0700 Subject: [PATCH] MV Auth - Early WIP --- Directory.Packages.props | 1 + .../Console/Commands/LauncherAuthCommand.cs | 39 +++++-- Robust.Shared/CVars.cs | 7 -- Robust.Shared/Network/AuthManager.cs | 33 ++---- .../Handshake/MsgEncryptionResponse.cs | 11 +- .../Messages/Handshake/MsgLoginStart.cs | 17 ++- .../Network/NetManager.ClientConnect.cs | 38 +++---- .../Network/NetManager.ServerAuth.cs | 105 +++++++++++++----- Robust.Shared/Robust.Shared.csproj | 1 + 9 files changed, 162 insertions(+), 90 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index aead66c49ed..4b924f52a37 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,6 +5,7 @@ + diff --git a/Robust.Client/Console/Commands/LauncherAuthCommand.cs b/Robust.Client/Console/Commands/LauncherAuthCommand.cs index 27d1469aad8..c558667c54f 100644 --- a/Robust.Client/Console/Commands/LauncherAuthCommand.cs +++ b/Robust.Client/Console/Commands/LauncherAuthCommand.cs @@ -1,6 +1,9 @@ #if TOOLS using System; using System.IO; +using System.Security.Cryptography; +using JWT.Algorithms; +using JWT.Builder; using Microsoft.Data.Sqlite; using Robust.Client.Utility; using Robust.Shared.Console; @@ -21,7 +24,8 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) var wantName = args.Length > 0 ? args[0] : null; var basePath = Path.GetDirectoryName(UserDataDir.GetUserDataDir(_gameController))!; - var dbPath = Path.Combine(basePath, "launcher", "settings.db"); + //var dbPath = Path.Combine(basePath, "launcher-ssmv", "settings.db"); + var dbPath = Path.Combine(basePath, "Test61", "settings.db"); // TEMP #if USE_SYSTEM_SQLITE SQLitePCL.raw.SetProvider(new SQLitePCL.SQLite3Provider_sqlite3()); @@ -29,11 +33,11 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) using var con = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly"); con.Open(); using var cmd = con.CreateCommand(); - cmd.CommandText = "SELECT UserId, UserName, Token FROM Login WHERE Expires > datetime('NOW')"; + cmd.CommandText = "SELECT UserName, PublicKey, PrivateKey FROM LoginMV"; if (wantName != null) { - cmd.CommandText += " AND UserName = @userName"; + cmd.CommandText += " WHERE UserName = @userName"; cmd.Parameters.AddWithValue("@userName", wantName); } @@ -47,14 +51,31 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) return; } - var userId = Guid.Parse(reader.GetString(0)); - var userName = reader.GetString(1); - var token = reader.GetString(2); + var userName = reader.GetString(0); + var publicKeyString = reader.GetString(1); + var privateKeyString = reader.GetString(2); - _auth.Token = token; - _auth.UserId = new NetUserId(userId); + var publicKey = RSA.Create(); + publicKey.ImportFromPem(publicKeyString); - shell.WriteLine($"Logged into account {userName}"); + var privateKey = RSA.Create(); + privateKey.ImportFromPem(privateKeyString); + + // Create JWT + var token = JwtBuilder.Create() + .WithAlgorithm(new RS2048Algorithm(publicKey, privateKey)) + .AddClaim("exp", DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds()) // expiry + .AddClaim("nbf", DateTimeOffset.UtcNow.AddMinutes(-5).ToUnixTimeSeconds()) // not before + .AddClaim("iat", DateTimeOffset.UtcNow) // issued at + .AddClaim("jti", "TODO") // TODO + .AddClaim("aud", "TODO") // TODO + .AddClaim("preferredUserName", userName) + .Encode(); + + _auth.UserJWT = token; + _auth.UserPublicKey = publicKeyString; + + shell.WriteLine($"Set auth parameters based on launcher keys for {userName}"); } } } diff --git a/Robust.Shared/CVars.cs b/Robust.Shared/CVars.cs index 01ac866d4b8..0a1c216ebb3 100644 --- a/Robust.Shared/CVars.cs +++ b/Robust.Shared/CVars.cs @@ -860,13 +860,6 @@ protected CVars() public static readonly CVarDef AuthAllowLocal = CVarDef.Create("auth.allowlocal", true, CVar.SERVERONLY); - // Only respected on server, client goes through IAuthManager for security. - /// - /// Authentication server address. - /// - public static readonly CVarDef AuthServer = - CVarDef.Create("auth.server", AuthManager.DefaultAuthServer, CVar.SERVERONLY); - /* * RENDERING */ diff --git a/Robust.Shared/Network/AuthManager.cs b/Robust.Shared/Network/AuthManager.cs index a397678d4b9..2c3a11cae68 100644 --- a/Robust.Shared/Network/AuthManager.cs +++ b/Robust.Shared/Network/AuthManager.cs @@ -10,43 +10,34 @@ namespace Robust.Shared.Network /// internal interface IAuthManager { - NetUserId? UserId { get; set; } - string? Server { get; set; } - string? Token { get; set; } - string? PubKey { get; set; } + string? ServerPublicKey { get; set; } + string? UserPublicKey { get; set; } + string? UserJWT { get; set; } void LoadFromEnv(); } internal sealed class AuthManager : IAuthManager { - public const string DefaultAuthServer = "https://central.spacestation14.io/auth/"; - - public NetUserId? UserId { get; set; } - public string? Server { get; set; } = DefaultAuthServer; - public string? Token { get; set; } - public string? PubKey { get; set; } + public string? ServerPublicKey { get; set; } + public string? UserPublicKey { get; set; } + public string? UserJWT { get; set; } public void LoadFromEnv() { - if (TryGetVar("ROBUST_AUTH_SERVER", out var server)) - { - Server = server; - } - - if (TryGetVar("ROBUST_AUTH_USERID", out var userId)) + if (TryGetVar("ROBUST_AUTH_PUBKEY", out var pubKey)) // Server's public key { - UserId = new NetUserId(Guid.Parse(userId)); + ServerPublicKey = pubKey; } - if (TryGetVar("ROBUST_AUTH_PUBKEY", out var pubKey)) + if (TryGetVar("ROBUST_USER_PUBLIC_KEY", out var userPublicKey)) // User's public key { - PubKey = pubKey; + UserPublicKey = userPublicKey; } - if (TryGetVar("ROBUST_AUTH_TOKEN", out var token)) + if (TryGetVar("ROBUST_USER_JWT", out var userJWT)) { - Token = token; + UserJWT = userJWT; } static bool TryGetVar(string var, [NotNullWhen(true)] out string? val) diff --git a/Robust.Shared/Network/Messages/Handshake/MsgEncryptionResponse.cs b/Robust.Shared/Network/Messages/Handshake/MsgEncryptionResponse.cs index 0442a16f0df..383830bb386 100644 --- a/Robust.Shared/Network/Messages/Handshake/MsgEncryptionResponse.cs +++ b/Robust.Shared/Network/Messages/Handshake/MsgEncryptionResponse.cs @@ -12,21 +12,26 @@ internal sealed class MsgEncryptionResponse : NetMessage public override MsgGroups MsgGroup => MsgGroups.Core; - public Guid UserId; public byte[] SealedData; + public string UserJWT; + public string UserPublicKey; public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { - UserId = buffer.ReadGuid(); var keyLength = buffer.ReadVariableInt32(); SealedData = buffer.ReadBytes(keyLength); + + UserJWT = buffer.ReadString(); + UserPublicKey = buffer.ReadString(); } public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { - buffer.Write(UserId); buffer.WriteVariableInt32(SealedData.Length); buffer.Write(SealedData); + + buffer.Write(UserJWT); + buffer.Write(UserPublicKey); } } } diff --git a/Robust.Shared/Network/Messages/Handshake/MsgLoginStart.cs b/Robust.Shared/Network/Messages/Handshake/MsgLoginStart.cs index 21e2f790e3d..a64f0b319bf 100644 --- a/Robust.Shared/Network/Messages/Handshake/MsgLoginStart.cs +++ b/Robust.Shared/Network/Messages/Handshake/MsgLoginStart.cs @@ -15,29 +15,34 @@ internal sealed class MsgLoginStart : NetMessage public override MsgGroups MsgGroup => MsgGroups.Core; - public string UserName; + /// + /// This is the username the player prefers -- however, the server may end up assigning a + /// derivative based on it. + /// + public string PreferredUserName; + public ImmutableArray HWId; public bool CanAuth; - public bool NeedPubKey; + public bool NeedServerPublicKey; public bool Encrypt; public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { - UserName = buffer.ReadString(); + PreferredUserName = buffer.ReadString(); var length = buffer.ReadByte(); HWId = ImmutableArray.Create(buffer.ReadBytes(length)); CanAuth = buffer.ReadBoolean(); - NeedPubKey = buffer.ReadBoolean(); + NeedServerPublicKey = buffer.ReadBoolean(); Encrypt = buffer.ReadBoolean(); } public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { - buffer.Write(UserName); + buffer.Write(PreferredUserName); buffer.Write((byte) HWId.Length); buffer.Write(HWId.AsSpan()); buffer.Write(CanAuth); - buffer.Write(NeedPubKey); + buffer.Write(NeedServerPublicKey); buffer.Write(Encrypt); } } diff --git a/Robust.Shared/Network/NetManager.ClientConnect.cs b/Robust.Shared/Network/NetManager.ClientConnect.cs index dd735c23d80..e8ef66f7084 100644 --- a/Robust.Shared/Network/NetManager.ClientConnect.cs +++ b/Robust.Shared/Network/NetManager.ClientConnect.cs @@ -124,20 +124,19 @@ private async Task CCDoHandshake(NetPeerData peer, NetConnection connection, str CancellationToken cancel) { var encrypt = _config.GetCVar(CVars.NetEncrypt); - var authToken = _authManager.Token; - var pubKey = _authManager.PubKey; - var authServer = _authManager.Server; - var userId = _authManager.UserId; + var serverPublicKey = _authManager.ServerPublicKey; + var userPublicKey = _authManager.UserPublicKey; + var userJWT = _authManager.UserJWT; - var hasPubKey = !string.IsNullOrEmpty(pubKey); - var authenticate = !string.IsNullOrEmpty(authToken); + var hasServerPublicKey = !string.IsNullOrEmpty(serverPublicKey); + var authenticate = !string.IsNullOrEmpty(userPublicKey) && !string.IsNullOrEmpty(userJWT); var hwId = ImmutableArray.Create(HWId.Calc()); var msgLogin = new MsgLoginStart { - UserName = userNameRequest, + PreferredUserName = userNameRequest, CanAuth = authenticate, - NeedPubKey = !hasPubKey, + NeedServerPublicKey = !hasServerPublicKey, HWId = hwId, Encrypt = encrypt }; @@ -163,10 +162,10 @@ private async Task CCDoHandshake(NetPeerData peer, NetConnection connection, str encryption = new NetEncryption(sharedSecret, isServer: false); byte[] keyBytes; - if (hasPubKey) + if (hasServerPublicKey) { // public key provided by launcher. - keyBytes = Convert.FromBase64String(pubKey!); + keyBytes = Convert.FromBase64String(serverPublicKey!); } else { @@ -188,21 +187,22 @@ private async Task CCDoHandshake(NetPeerData peer, NetConnection connection, str var sealedData = CryptoBox.Seal(data, keyBytes); - var authHashBytes = MakeAuthHash(sharedSecret, keyBytes); - var authHash = Convert.ToBase64String(authHashBytes); + // var authHashBytes = MakeAuthHash(sharedSecret, keyBytes); + // var authHash = Convert.ToBase64String(authHashBytes); - var joinReq = new JoinRequest(authHash); - var request = new HttpRequestMessage(HttpMethod.Post, authServer + "api/session/join"); - request.Content = JsonContent.Create(joinReq); - request.Headers.Authorization = new AuthenticationHeaderValue("SS14Auth", authToken); - var joinResp = await _http.Client.SendAsync(request, cancel); + // var joinReq = new JoinRequest(authHash); + // var request = new HttpRequestMessage(HttpMethod.Post, authServer + "api/session/join"); + // request.Content = JsonContent.Create(joinReq); + // request.Headers.Authorization = new AuthenticationHeaderValue("SS14Auth", authToken); + // var joinResp = await _http.Client.SendAsync(request, cancel); - joinResp.EnsureSuccessStatusCode(); + // joinResp.EnsureSuccessStatusCode(); var encryptionResponse = new MsgEncryptionResponse { SealedData = sealedData, - UserId = userId!.Value.UserId + UserJWT = _authManager.UserJWT, + UserPublicKey = _authManager.UserPublicKey }; var outEncRespMsg = peer.Peer.CreateMessage(); diff --git a/Robust.Shared/Network/NetManager.ServerAuth.cs b/Robust.Shared/Network/NetManager.ServerAuth.cs index 8296c446dda..3f5a17be79b 100644 --- a/Robust.Shared/Network/NetManager.ServerAuth.cs +++ b/Robust.Shared/Network/NetManager.ServerAuth.cs @@ -5,6 +5,11 @@ using System.Net.Http.Json; using System.Security.Cryptography; using System.Threading.Tasks; +using JWT; +using JWT.Algorithms; +using JWT.Builder; +using JWT.Exceptions; +using JWT.Serializers; using Lidgren.Network; using Robust.Shared.AuthLib; using Robust.Shared.Log; @@ -48,12 +53,11 @@ private async void HandleHandshake(NetPeerData peer, NetConnection connection) var ip = connection.RemoteEndPoint.Address; var isLocal = IPAddress.IsLoopback(ip) && _config.GetCVar(CVars.AuthAllowLocal); var canAuth = msgLogin.CanAuth; - var needPk = msgLogin.NeedPubKey; - var authServer = _config.GetCVar(CVars.AuthServer); + var needServerPublicKey = msgLogin.NeedServerPublicKey; _logger.Verbose( $"{connection.RemoteEndPoint}: Received MsgLoginStart. " + - $"canAuth: {canAuth}, needPk: {needPk}, username: {msgLogin.UserName}, encrypt: {msgLogin.Encrypt}"); + $"canAuth: {canAuth}, needServerPublicKey: {needServerPublicKey}, username: {msgLogin.PreferredUserName}, encrypt: {msgLogin.Encrypt}"); _logger.Verbose( $"{connection.RemoteEndPoint}: Connection is specialized local? {isLocal} "); @@ -81,7 +85,7 @@ private async void HandleHandshake(NetPeerData peer, NetConnection connection) RandomNumberGenerator.Fill(verifyToken); var msgEncReq = new MsgEncryptionRequest { - PublicKey = needPk ? CryptoPublicKey : Array.Empty(), + PublicKey = needServerPublicKey ? CryptoPublicKey : Array.Empty(), VerifyToken = verifyToken }; @@ -131,42 +135,93 @@ private async void HandleHandshake(NetPeerData peer, NetConnection connection) if (msgLogin.Encrypt) encryption = new NetEncryption(sharedSecret, isServer: true); - _logger.Verbose( - $"{connection.RemoteEndPoint}: Checking with session server for auth hash..."); + // Validate the JWT + var userPublicKeyString = msgEncResponse.UserPublicKey ?? ""; + var userJWTString = msgEncResponse.UserJWT ?? ""; + + var userPublicKey = RSA.Create(); + userPublicKey.ImportFromPem(userPublicKeyString); - var authHashBytes = MakeAuthHash(sharedSecret, CryptoPublicKey!); - var authHash = Base64Helpers.ConvertToBase64Url(authHashBytes); + try + { + IJsonSerializer serializer = new JsonNetSerializer(); + IDateTimeProvider provider = new UtcDateTimeProvider(); + IJwtValidator validator = new JwtValidator(serializer, provider); + IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); + IJwtAlgorithm algorithm = new RS2048Algorithm(userPublicKey); + IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm); - var url = $"{authServer}api/session/hasJoined?hash={authHash}&userId={msgEncResponse.UserId}"; - var joinedRespJson = await _http.Client.GetFromJsonAsync(url); + var jwtJson = decoder.Decode(userJWTString); - if (joinedRespJson is not {IsValid: true}) + } + catch (TokenNotYetValidException) + { + connection.Disconnect("JWT Validation Error - Token is not valid yet."); + return; + } + catch (TokenExpiredException) + { + connection.Disconnect("JWT Validation Error - Token has expired."); + return; + } + catch (SignatureVerificationException) + { + connection.Disconnect("JWT Validation Error - Token has invalid signature."); + return; + } + catch (Exception e) { - connection.Disconnect("Failed to validate login"); + connection.Disconnect("Misc JWT Error."); + _logger.Error("Misc JWT Error on user attempting to connect.", e); return; } _logger.Verbose( - $"{connection.RemoteEndPoint}: Auth hash passed. " + - $"User ID: {joinedRespJson.UserData!.UserId}, " + - $"Username: {joinedRespJson.UserData!.UserName}," + - $"Patron: {joinedRespJson.UserData.PatronTier}"); + $"{connection.RemoteEndPoint}: JWT appears valid"); - var userId = new NetUserId(joinedRespJson.UserData!.UserId); - userData = new NetUserData(userId, joinedRespJson.UserData.UserName) - { - PatronTier = joinedRespJson.UserData.PatronTier, - HWId = msgLogin.HWId - }; - padSuccessMessage = false; - type = LoginType.LoggedIn; + // TODO - Find user based on public key + + // _logger.Verbose( + // $"{connection.RemoteEndPoint}: Checking with session server for auth hash..."); + + // var authHashBytes = MakeAuthHash(sharedSecret, CryptoPublicKey!); + // var authHash = Base64Helpers.ConvertToBase64Url(authHashBytes); + + // var url = $"{authServer}api/session/hasJoined?hash={authHash}&userId={msgEncResponse.UserId}"; + // var joinedRespJson = await _http.Client.GetFromJsonAsync(url); + + // if (joinedRespJson is not {IsValid: true}) + // { + // connection.Disconnect("Failed to validate login"); + // return; + // } + + // _logger.Verbose( + // $"{connection.RemoteEndPoint}: Auth hash passed. " + + // $"User ID: {joinedRespJson.UserData!.UserId}, " + + // $"Username: {joinedRespJson.UserData!.UserName}," + + // $"Patron: {joinedRespJson.UserData.PatronTier}"); + + // TODO ASSIGNMENT:::: + + // var userId = new NetUserId(joinedRespJson.UserData!.UserId); + // userData = new NetUserData(userId, joinedRespJson.UserData.UserName) + // { + // PatronTier = joinedRespJson.UserData.PatronTier, + // HWId = msgLogin.HWId + // }; + // padSuccessMessage = false; + // type = LoginType.LoggedIn; + + connection.Disconnect("rawr"); + return; } else { _logger.Verbose( $"{connection.RemoteEndPoint}: Not doing authentication"); - var reqUserName = msgLogin.UserName; + var reqUserName = msgLogin.PreferredUserName; if (!UsernameHelpers.IsNameValid(reqUserName, out var reason)) { diff --git a/Robust.Shared/Robust.Shared.csproj b/Robust.Shared/Robust.Shared.csproj index 333cbd5a936..2a2d29d8041 100644 --- a/Robust.Shared/Robust.Shared.csproj +++ b/Robust.Shared/Robust.Shared.csproj @@ -8,6 +8,7 @@ +