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