Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions UnitystationLauncher/Models/Api/CharacterTokenResponse.cs

This file was deleted.

9 changes: 9 additions & 0 deletions UnitystationLauncher/Models/Api/ScopeTokenResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Text.Json.Serialization;

namespace UnitystationLauncher.Models.Api;

public class ScopeTokenResponse : JsonObject
{
[JsonPropertyName("scope_token")]
public string ScopeToken { get; set; } = string.Empty;
}
2 changes: 1 addition & 1 deletion UnitystationLauncher/Models/Api/Server.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public Server(string forkName, int buildVersion, string serverIp, int serverPort
public string GoodFileVersion { get; set; } = string.Empty;
public string ServerPublicKey { get; set; }

public string ServerConnectionPublicKey { get; set; }
public string DEPRECATEME_ServerConnectionPublicKey { get; set; }

public (string, int) ForkAndVersion => (ForkName, BuildVersion);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
namespace UnitystationLauncher.Models.Api;
using System.Text.Json.Serialization;

namespace UnitystationLauncher.Models.Api;

public class ServerConnectionAuthenticationRequest
{
public string? ClientFork { get; set; }

public string? ClientVersion { get; set; }

public string? GoodFileVersion { get; set; }
public string? EncryptedSharedSecret { get; set; }
public string? EncryptedAccountID { get; set; }

public string? ConnectionPublicServerKey { get; set; }
[JsonPropertyName("shared_secret")]
public string SharedSecret { get; set; } = string.Empty;

[JsonPropertyName("unique_identifier")]
public string UniqueIdentifier { get; set; } = string.Empty;

[JsonPropertyName("auth_realm")]
public string? AuthRealm { get; set; }
}
10 changes: 6 additions & 4 deletions UnitystationLauncher/Models/AuthenticationStuff.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,17 @@
}

[Serializable]
public class Registersha512token : JsonObject
public class ConnectionChallengeModel : JsonObject
{
[JsonProperty("sha512_token")]
public string? sha512_token { get; set; }
[JsonProperty("connection_challenge")]
public string? ConnectionChallenge { get; set; }
[JsonProperty("fork_compatibility")]
public string? ForkCompatibility { get; set; }
}


[Serializable]
public class GetCharacterForkToken : JsonObject
public class DEPRECATEME_GetCharacterForkToken : JsonObject

Check notice on line 60 in UnitystationLauncher/Models/AuthenticationStuff.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

UnitystationLauncher/Models/AuthenticationStuff.cs#L60

Rename class 'DEPRECATEME_GetCharacterForkToken' to match pascal case naming rules, consider using 'DeprecatemeGetCharacterForkToken'.
{
[JsonProperty("fork_compatibility")]
public string? fork_compatibility { get; set; }
Expand Down
18 changes: 7 additions & 11 deletions UnitystationLauncher/Services/AuthService.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.Mail;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
using UnitystationLauncher.Constants;
using UnitystationLauncher.Models;
using UnitystationLauncher.Models.Api;
using UnitystationLauncher.Models.ConfigFile;
using UnitystationLauncher.Services.Interface;

namespace UnitystationLauncher.Services
Expand Down Expand Up @@ -68,14 +63,15 @@ public void SaveAuthSettings()
}
}

public async Task<CharacterTokenResponse> GenerateCharacterSheetTokenForFork(string Fork)
public async Task<ScopeTokenResponse> RegisterConnectionChallenge(string ConnectionChallenge, string ForkCompatibility)
{
return (await _IAuthProvider.GenerateCharacterSheetTokenForFork(AccountLoginResponse.Token, Fork)).Data;
}
if(AccountLoginResponse == null)
{
Log.Error("Tried to register connection challenge with null AccountLoginResponse");
throw new InvalidOperationException("AccountLoginResponse is null. Cannot register connection challenge.");
}

public void RegisterJoiningServerWithSecret(string SharedSecret)
{
_IAuthProvider.SendRegisterSharedSecret(AccountLoginResponse.Token, SharedSecret);
return (await _IAuthProvider.SendRegisterConnectionChallenge(AccountLoginResponse.Token, ConnectionChallenge, ForkCompatibility)).Data;
}

public void ResendVerificationEmail(string email)
Expand Down
5 changes: 1 addition & 4 deletions UnitystationLauncher/Services/InstallationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -431,8 +431,7 @@ private async Task<string> GetArguments(Installation Installation, string? serve

if (string.IsNullOrWhiteSpace(server) == false)
{
var Arguments = await _IServerAuthenticationService.AuthenticateWithServer(server, Installation);

var Arguments = await _IServerAuthenticationService.PrenegotiateWithServer(server, Installation);

foreach (var Argument in Arguments)
{
Expand All @@ -451,8 +450,6 @@ private async Task<string> GetArguments(Installation Installation, string? serve
var Username = _authService.AccountLoginResponse.Account.Username;
arguments.Append($"-AccountID {AccountID} ");
arguments.Append($"-Username {Username} ");
var CharacterToken = await _authService.GenerateCharacterSheetTokenForFork(Installation.ForkName);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed for launching the client directly if you're hosting all playing single player so you can modify your character settings In that scenario

arguments.Append($"-CharacterToken {CharacterToken.token} ");
return arguments.ToString();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,15 @@ public Task<ApiResult<AccountRegisterResponse>> Register(

public Task<ApiResult<JsonObject>> SendForgotPasswordEmail(string email);

public Task<ApiResult<JsonObject>> SendRegisterSharedSecret(string token, string SharedSecret);

public Task<ApiResult<CharacterTokenResponse>> GenerateCharacterSheetTokenForFork(string token, string ForkName);
public Task<ApiResult<ScopeTokenResponse>> SendRegisterConnectionChallenge(string AccountToken, string ConnectionChallenge, string ForkCompatibility);
}

public class OfficialCentralCommandAuthentication : IAuthProvider
{

public static string Host => ApiUrls.ApiBaseUrlLogin;
private static UriBuilder UriBuilder = new(Host);
public static Uri GetUri(string endpoint, string? queries = null, string BeginningOverride = null)
public static Uri GetUri(string endpoint, string? queries = null, string BeginningOverride = "")
{

UriBuilder.Path = $"/accounts/{endpoint}";
Expand Down Expand Up @@ -162,41 +160,17 @@ public async Task<ApiResult<JsonObject>> SendForgotPasswordEmail(string email)
}
}

public async Task<ApiResult<JsonObject>> SendRegisterSharedSecret(string token, string SharedSecret)
{
try
{
var requestBody = new Registersha512token
{
sha512_token = SharedSecret,
};

var response = await ApiServer.Post<JsonObject>(GetUri("register-SHA512-for-account/"), requestBody, token);

if (response.IsSuccess == false)
{
throw response.Exception!;
}

return response;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}

public async Task<ApiResult<CharacterTokenResponse>> GenerateCharacterSheetTokenForFork(string token, string ForkName)
public async Task<ApiResult<ScopeTokenResponse>> SendRegisterConnectionChallenge(string AccountToken, string ConnectionChallenge, string ForkCompatibility)
{
try
{
var requestBody = new GetCharacterForkToken
var requestBody = new ConnectionChallengeModel
{
fork_compatibility = ForkName,
ConnectionChallenge = ConnectionChallenge,
ForkCompatibility = ForkCompatibility,
};

var response = await ApiServer.Post<CharacterTokenResponse>(GetUri("GenForkToken", BeginningOverride: "/persistence/characters/"), requestBody, token);
var response = await ApiServer.Post<ScopeTokenResponse>(GetUri("auth-request/"), requestBody, AccountToken);

if (response.IsSuccess == false)
{
Expand Down
69 changes: 36 additions & 33 deletions UnitystationLauncher/Services/ServerAuthenticationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Math.EC;
using Org.BouncyCastle.OpenSsl;
using UnitystationLauncher.Models;
using UnitystationLauncher.Models.Api;
Expand All @@ -22,9 +23,9 @@ namespace UnitystationLauncher.Services;

public interface IServerAuthenticationService
{
public Task<Server> GetServerInfoByIP(string IP);
public Task<Server> QueryServerInfo(string IP);

public Task<Dictionary<string, string>> AuthenticateWithServer(string IP, Installation Installation);
public Task<Dictionary<string, string>> PrenegotiateWithServer(string IP, Installation Installation);
}

public class ServerAuthenticationService : IServerAuthenticationService
Expand All @@ -38,13 +39,15 @@ public ServerAuthenticationService(AuthService AuthService)
private readonly AuthService _AuthService;
private readonly HttpClient _httpClient = new HttpClient();

private readonly SHA512 SHA512 = SHA512.Create();

public async Task<Server> GetServerInfoByIP(string IP)
public async Task<Server> QueryServerInfo(string IP)
{
try
{
string Port = "7778";
// check if IP is in the format "IP:Port"
// if not, assume default port 7778
string[] parts = IP.Split(':');
string Port = parts.Length == 2 ? parts[1] : "7778";

string url = $"http://{IP}:{Port}/";

var response = await _httpClient.GetAsync(url);
Expand Down Expand Up @@ -72,50 +75,48 @@ static AsymmetricKeyParameter ImportKeyFromPem(string pem)
}


public async Task<Dictionary<string, string>> AuthenticateWithServer(string IP, Installation Installation)
public async Task<Dictionary<string, string>> PrenegotiateWithServer(string IP, Installation Installation)
{
var Info = await GetServerInfoByIP(IP);
var RSAEncrypt = new OaepEncoding(
new RsaEngine(),
var Info = await QueryServerInfo(IP);
var CryptoEncoding = new OaepEncoding(
new ElGamalEngine(),
new Sha256Digest()
);

RSAEncrypt.Init(true, ImportKeyFromPem(Info.ServerPublicKey)); // false = decrypt mode
// wrap the base64 encoded public key in PEM format
var ServerPublicKey = ImportKeyFromPem("-----BEGIN PUBLIC KEY-----\n" +
Info.ServerPublicKey + "\n" +
"-----END PUBLIC KEY-----\n");

CryptoEncoding.Init(true, ServerPublicKey); // false = decrypt mode

byte[] sharedSecret = new byte[32]; // 256-bit key
RandomNumberGenerator.Fill(sharedSecret);

// Optional: convert to Base64 if you want to transmit/store it
string base64Secret = Convert.ToBase64String(sharedSecret);
var hashInput = new byte[64];
Array.Copy(sharedSecret, 0, hashInput, 0, 32);
Array.Copy(Convert.FromBase64String(Info.ServerPublicKey), 0, hashInput, 32, 32);

var SHA512Check = Convert.ToBase64String(
SHA512.ComputeHash(
Encoding.UTF8.GetBytes(
base64Secret
+ Info.ServerPublicKey
+ Installation.BuildVersion.ToString()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to send all this crap as part of the token since the malicious server can spoof these to be something else

+ Installation.ForkName.ToString()
+ Installation.GoodFileVersion.ToString()
+ Info.ServerConnectionPublicKey)));
_AuthService.RegisterJoiningServerWithSecret(SHA512Check);
var ConnectionChallenge = Convert.ToHexString(SHA512.HashData(hashInput));

var ScopeToken = await _AuthService.RegisterConnectionChallenge(ConnectionChallenge, Installation.ForkName);


var ToSend = new ServerConnectionAuthenticationRequest
{
ConnectionPublicServerKey = Info.ServerConnectionPublicKey,
ClientVersion = Installation.BuildVersion.ToString(),
GoodFileVersion = Installation.GoodFileVersion.ToString(),
ClientFork = Installation.ForkName.ToString(),
EncryptedSharedSecret = EncryptString(RSAEncrypt, base64Secret),
EncryptedAccountID = EncryptString(RSAEncrypt, _AuthService.AccountLoginResponse.Account.UniqueIdentifier)
SharedSecret = Convert.ToBase64String(sharedSecret),
UniqueIdentifier = _AuthService.AccountLoginResponse.Account.UniqueIdentifier,
AuthRealm = _AuthService
};


// Serialize the object to JSON
string json = JsonConvert.SerializeObject(ToSend);

// Wrap it in a StringContent with JSON media type
var content = new StringContent(json, Encoding.UTF8, "application/json");
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");

//TODO make this into a function like QueryServerIP

string Port = "7778";
string url = $"http://{IP}:{Port}/";
Expand All @@ -128,17 +129,19 @@ public async Task<Dictionary<string, string>> AuthenticateWithServer(string IP,
{
throw new AuthenticationException(contentBack + $" When trying to authenticate with {url}");
}
//end of todo

return new Dictionary<string, string>
{
{"-SharedSecret", base64Secret},
{"-ServerPublicConnectionKey", Info.ServerConnectionPublicKey},
{"-ServerPublicConnectionKey", Info.DEPRECATEME_ServerConnectionPublicKey},
{"-ScopedToken", ScopeToken.ScopeToken}
};
}

private string EncryptString(OaepEncoding rsa, string ToEncrypt)
private string EncryptString(OaepEncoding encoding, string ToEncrypt)
{
var Bytes = Encoding.UTF8.GetBytes(ToEncrypt);
return Convert.ToBase64String(rsa.ProcessBlock(Bytes, 0, Bytes.Length));
return Convert.ToBase64String(encoding.ProcessBlock(Bytes, 0, Bytes.Length));
}
}