diff --git a/UnitystationLauncher/Models/Api/CharacterTokenResponse.cs b/UnitystationLauncher/Models/Api/CharacterTokenResponse.cs deleted file mode 100644 index 32b9e53..0000000 --- a/UnitystationLauncher/Models/Api/CharacterTokenResponse.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace UnitystationLauncher.Models.Api; - -public class CharacterTokenResponse : JsonObject -{ - public string token { get; set; } -} \ No newline at end of file diff --git a/UnitystationLauncher/Models/Api/ScopeTokenResponse.cs b/UnitystationLauncher/Models/Api/ScopeTokenResponse.cs new file mode 100644 index 0000000..b4913a6 --- /dev/null +++ b/UnitystationLauncher/Models/Api/ScopeTokenResponse.cs @@ -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; +} \ No newline at end of file diff --git a/UnitystationLauncher/Models/Api/Server.cs b/UnitystationLauncher/Models/Api/Server.cs index 206b4f3..89a62c2 100644 --- a/UnitystationLauncher/Models/Api/Server.cs +++ b/UnitystationLauncher/Models/Api/Server.cs @@ -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); diff --git a/UnitystationLauncher/Models/Api/ServerConnectionAuthenticationRequest.cs b/UnitystationLauncher/Models/Api/ServerConnectionAuthenticationRequest.cs index ae223b3..ee9a6e5 100644 --- a/UnitystationLauncher/Models/Api/ServerConnectionAuthenticationRequest.cs +++ b/UnitystationLauncher/Models/Api/ServerConnectionAuthenticationRequest.cs @@ -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; } } \ No newline at end of file diff --git a/UnitystationLauncher/Models/AuthenticationStuff.cs b/UnitystationLauncher/Models/AuthenticationStuff.cs index 47730cf..1125085 100644 --- a/UnitystationLauncher/Models/AuthenticationStuff.cs +++ b/UnitystationLauncher/Models/AuthenticationStuff.cs @@ -47,15 +47,17 @@ public class ForgotPasswordModel : JsonObject } [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 { [JsonProperty("fork_compatibility")] public string? fork_compatibility { get; set; } diff --git a/UnitystationLauncher/Services/AuthService.cs b/UnitystationLauncher/Services/AuthService.cs index 388590e..07c729a 100644 --- a/UnitystationLauncher/Services/AuthService.cs +++ b/UnitystationLauncher/Services/AuthService.cs @@ -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 @@ -68,14 +63,15 @@ public void SaveAuthSettings() } } - public async Task GenerateCharacterSheetTokenForFork(string Fork) + public async Task 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) diff --git a/UnitystationLauncher/Services/InstallationService.cs b/UnitystationLauncher/Services/InstallationService.cs index 5234c1b..f252591 100644 --- a/UnitystationLauncher/Services/InstallationService.cs +++ b/UnitystationLauncher/Services/InstallationService.cs @@ -431,8 +431,7 @@ private async Task 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) { @@ -451,8 +450,6 @@ private async Task 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); - arguments.Append($"-CharacterToken {CharacterToken.token} "); return arguments.ToString(); } diff --git a/UnitystationLauncher/Services/OfficialCentralCommandAuthentication.cs b/UnitystationLauncher/Services/OfficialCentralCommandAuthentication.cs index c056aef..9239ad8 100644 --- a/UnitystationLauncher/Services/OfficialCentralCommandAuthentication.cs +++ b/UnitystationLauncher/Services/OfficialCentralCommandAuthentication.cs @@ -20,9 +20,7 @@ public Task> Register( public Task> SendForgotPasswordEmail(string email); - public Task> SendRegisterSharedSecret(string token, string SharedSecret); - - public Task> GenerateCharacterSheetTokenForFork(string token, string ForkName); + public Task> SendRegisterConnectionChallenge(string AccountToken, string ConnectionChallenge, string ForkCompatibility); } public class OfficialCentralCommandAuthentication : IAuthProvider @@ -30,7 +28,7 @@ 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}"; @@ -162,41 +160,17 @@ public async Task> SendForgotPasswordEmail(string email) } } - public async Task> SendRegisterSharedSecret(string token, string SharedSecret) - { - try - { - var requestBody = new Registersha512token - { - sha512_token = SharedSecret, - }; - - var response = await ApiServer.Post(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> GenerateCharacterSheetTokenForFork(string token, string ForkName) + public async Task> 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(GetUri("GenForkToken", BeginningOverride: "/persistence/characters/"), requestBody, token); + var response = await ApiServer.Post(GetUri("auth-request/"), requestBody, AccountToken); if (response.IsSuccess == false) { diff --git a/UnitystationLauncher/Services/ServerAuthenticationService.cs b/UnitystationLauncher/Services/ServerAuthenticationService.cs index c0943f7..ffd1449 100644 --- a/UnitystationLauncher/Services/ServerAuthenticationService.cs +++ b/UnitystationLauncher/Services/ServerAuthenticationService.cs @@ -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; @@ -22,9 +23,9 @@ namespace UnitystationLauncher.Services; public interface IServerAuthenticationService { - public Task GetServerInfoByIP(string IP); + public Task QueryServerInfo(string IP); - public Task> AuthenticateWithServer(string IP, Installation Installation); + public Task> PrenegotiateWithServer(string IP, Installation Installation); } public class ServerAuthenticationService : IServerAuthenticationService @@ -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 GetServerInfoByIP(string IP) + public async Task 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); @@ -72,42 +75,38 @@ static AsymmetricKeyParameter ImportKeyFromPem(string pem) } - public async Task> AuthenticateWithServer(string IP, Installation Installation) + public async Task> 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() - + 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 }; @@ -115,7 +114,9 @@ public async Task> AuthenticateWithServer(string IP, 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}/"; @@ -128,17 +129,19 @@ public async Task> AuthenticateWithServer(string IP, { throw new AuthenticationException(contentBack + $" When trying to authenticate with {url}"); } + //end of todo return new Dictionary { {"-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)); } } \ No newline at end of file