mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2025-11-07 17:36:07 +00:00
Refactoring to asynchronous. (partially completed)
This commit is contained in:
parent
7ee08092d4
commit
096ea0c70c
72 changed files with 6033 additions and 5080 deletions
|
|
@ -2,6 +2,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MinecraftClient.Protocol.Message;
|
||||
|
||||
namespace MinecraftClient.Protocol.ProfileKey
|
||||
|
|
@ -10,51 +11,6 @@ namespace MinecraftClient.Protocol.ProfileKey
|
|||
{
|
||||
private static readonly SHA256 sha256Hash = SHA256.Create();
|
||||
|
||||
private static readonly string certificates = "https://api.minecraftservices.com/player/certificates";
|
||||
|
||||
public static PlayerKeyPair? GetNewProfileKeys(string accessToken)
|
||||
{
|
||||
ProxiedWebRequest.Response? response = null;
|
||||
try
|
||||
{
|
||||
var request = new ProxiedWebRequest(certificates)
|
||||
{
|
||||
Accept = "application/json"
|
||||
};
|
||||
request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
|
||||
|
||||
response = request.Post("application/json", "");
|
||||
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
{
|
||||
ConsoleIO.WriteLine(response.Body.ToString());
|
||||
}
|
||||
|
||||
string jsonString = response.Body;
|
||||
Json.JSONData json = Json.ParseJson(jsonString);
|
||||
|
||||
PublicKey publicKey = new(pemKey: json.Properties["keyPair"].Properties["publicKey"].StringValue,
|
||||
sig: json.Properties["publicKeySignature"].StringValue,
|
||||
sigV2: json.Properties["publicKeySignatureV2"].StringValue);
|
||||
|
||||
PrivateKey privateKey = new(pemKey: json.Properties["keyPair"].Properties["privateKey"].StringValue);
|
||||
|
||||
return new PlayerKeyPair(publicKey, privateKey,
|
||||
expiresAt: json.Properties["expiresAt"].StringValue,
|
||||
refreshedAfter: json.Properties["refreshedAfter"].StringValue);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
int code = response == null ? 0 : response.StatusCode;
|
||||
ConsoleIO.WriteLineFormatted("§cFetch profile key failed: HttpCode = " + code + ", Error = " + e.Message);
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§c" + e.StackTrace);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] DecodePemKey(string key, string prefix, string suffix)
|
||||
{
|
||||
int i = key.IndexOf(prefix);
|
||||
|
|
|
|||
|
|
@ -1,199 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
using System.Timers;
|
||||
using static MinecraftClient.Settings;
|
||||
using static MinecraftClient.Settings.MainConfigHealper.MainConfig.AdvancedConfig;
|
||||
|
||||
namespace MinecraftClient.Protocol.ProfileKey
|
||||
{
|
||||
/// <summary>
|
||||
/// Handle keys caching and storage.
|
||||
/// </summary>
|
||||
public static class KeysCache
|
||||
{
|
||||
private const string KeysCacheFilePlaintext = "ProfileKeyCache.ini";
|
||||
|
||||
private static FileMonitor? cachemonitor;
|
||||
private static readonly Dictionary<string, PlayerKeyPair> keys = new();
|
||||
private static readonly Timer updatetimer = new(100);
|
||||
private static readonly List<KeyValuePair<string, PlayerKeyPair>> pendingadds = new();
|
||||
private static readonly BinaryFormatter formatter = new();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve whether KeysCache contains a keys for the given login.
|
||||
/// </summary>
|
||||
/// <param name="login">User login used with Minecraft.net</param>
|
||||
/// <returns>TRUE if keys are available</returns>
|
||||
public static bool Contains(string login)
|
||||
{
|
||||
return keys.ContainsKey(login);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Store keys and save it to disk if required.
|
||||
/// </summary>
|
||||
/// <param name="login">User login used with Minecraft.net</param>
|
||||
/// <param name="playerKeyPair">User keys</param>
|
||||
public static void Store(string login, PlayerKeyPair playerKeyPair)
|
||||
{
|
||||
if (Contains(login))
|
||||
{
|
||||
keys[login] = playerKeyPair;
|
||||
}
|
||||
else
|
||||
{
|
||||
keys.Add(login, playerKeyPair);
|
||||
}
|
||||
|
||||
if (Config.Main.Advanced.ProfileKeyCache == CacheType.disk && updatetimer.Enabled == true)
|
||||
{
|
||||
pendingadds.Add(new KeyValuePair<string, PlayerKeyPair>(login, playerKeyPair));
|
||||
}
|
||||
else if (Config.Main.Advanced.ProfileKeyCache == CacheType.disk)
|
||||
{
|
||||
SaveToDisk();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve keys for the given login.
|
||||
/// </summary>
|
||||
/// <param name="login">User login used with Minecraft.net</param>
|
||||
/// <returns>PlayerKeyPair for given login</returns>
|
||||
public static PlayerKeyPair Get(string login)
|
||||
{
|
||||
return keys[login];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize cache monitoring to keep cache updated with external changes.
|
||||
/// </summary>
|
||||
/// <returns>TRUE if keys are seeded from file</returns>
|
||||
public static bool InitializeDiskCache()
|
||||
{
|
||||
cachemonitor = new FileMonitor(AppDomain.CurrentDomain.BaseDirectory, KeysCacheFilePlaintext, new FileSystemEventHandler(OnChanged));
|
||||
updatetimer.Elapsed += HandlePending;
|
||||
return LoadFromDisk();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads cache on external cache file change.
|
||||
/// </summary>
|
||||
/// <param name="sender">Sender</param>
|
||||
/// <param name="e">Event data</param>
|
||||
private static void OnChanged(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
updatetimer.Stop();
|
||||
updatetimer.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after timer elapsed. Reads disk cache and adds new/modified keys back.
|
||||
/// </summary>
|
||||
/// <param name="sender">Sender</param>
|
||||
/// <param name="e">Event data</param>
|
||||
private static void HandlePending(object? sender, ElapsedEventArgs e)
|
||||
{
|
||||
updatetimer.Stop();
|
||||
LoadFromDisk();
|
||||
|
||||
foreach (KeyValuePair<string, PlayerKeyPair> pending in pendingadds.ToArray())
|
||||
{
|
||||
Store(pending.Key, pending.Value);
|
||||
pendingadds.Remove(pending);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads cache file and loads KeysInfos into KeysCache.
|
||||
/// </summary>
|
||||
/// <returns>True if data is successfully loaded</returns>
|
||||
private static bool LoadFromDisk()
|
||||
{
|
||||
//User-editable keys cache file in text format
|
||||
if (File.Exists(KeysCacheFilePlaintext))
|
||||
{
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading_keys, KeysCacheFilePlaintext));
|
||||
|
||||
try
|
||||
{
|
||||
foreach (string line in FileMonitor.ReadAllLinesWithRetries(KeysCacheFilePlaintext))
|
||||
{
|
||||
if (!line.TrimStart().StartsWith("#"))
|
||||
{
|
||||
|
||||
int separatorIdx = line.IndexOf('=');
|
||||
if (separatorIdx >= 1 && line.Length > separatorIdx + 1)
|
||||
{
|
||||
string login = line[..separatorIdx];
|
||||
string value = line[(separatorIdx + 1)..];
|
||||
try
|
||||
{
|
||||
PlayerKeyPair playerKeyPair = PlayerKeyPair.FromString(value);
|
||||
keys[login] = playerKeyPair;
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded_keys, playerKeyPair.ExpiresAt.ToString()));
|
||||
}
|
||||
catch (InvalidDataException e)
|
||||
{
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string_keys, value, e.Message));
|
||||
}
|
||||
catch (FormatException e)
|
||||
{
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string_keys, value, e.Message));
|
||||
}
|
||||
catch (ArgumentNullException e)
|
||||
{
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string_keys, value, e.Message));
|
||||
|
||||
}
|
||||
}
|
||||
else if (Config.Logging.DebugMessages)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_line_keys, line));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain_keys, e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
return keys.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves player's keypair from KeysCache into cache file.
|
||||
/// </summary>
|
||||
private static void SaveToDisk()
|
||||
{
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted("§8" + Translations.cache_saving_keys, acceptnewlines: true);
|
||||
|
||||
List<string> KeysCacheLines = new()
|
||||
{
|
||||
"# Generated by MCC v" + Program.Version + " - Keep it secret & Edit at own risk!",
|
||||
"# ProfileKey=PublicKey(base64),PublicKeySignature(base64),PublicKeySignatureV2(base64),PrivateKey(base64),ExpiresAt,RefreshAfter"
|
||||
};
|
||||
foreach (KeyValuePair<string, PlayerKeyPair> entry in keys)
|
||||
KeysCacheLines.Add(entry.Key + '=' + entry.Value.ToString());
|
||||
|
||||
try
|
||||
{
|
||||
FileMonitor.WriteAllLinesWithRetries(KeysCacheFilePlaintext, KeysCacheLines);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_save_fail_keys, e.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +1,38 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MinecraftClient.Protocol.ProfileKey
|
||||
{
|
||||
public class PlayerKeyPair
|
||||
{
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("PublicKey")]
|
||||
public PublicKey PublicKey;
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("PrivateKey")]
|
||||
public PrivateKey PrivateKey;
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("ExpiresAt")]
|
||||
public DateTime ExpiresAt;
|
||||
|
||||
public DateTime RefreshedAfter; // Todo: add a timer
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("RefreshedAfter")]
|
||||
public DateTime RefreshedAfter;
|
||||
|
||||
[JsonIgnore]
|
||||
private const string DataTimeFormat = "yyyy-MM-ddTHH:mm:ss.ffffffZ";
|
||||
|
||||
public PlayerKeyPair(PublicKey keyPublic, PrivateKey keyPrivate, string expiresAt, string refreshedAfter)
|
||||
[JsonConstructor]
|
||||
public PlayerKeyPair(PublicKey PublicKey, PrivateKey PrivateKey, DateTime ExpiresAt, DateTime RefreshedAfter)
|
||||
{
|
||||
PublicKey = keyPublic;
|
||||
PrivateKey = keyPrivate;
|
||||
try
|
||||
{
|
||||
ExpiresAt = DateTime.ParseExact(expiresAt, DataTimeFormat, System.Globalization.CultureInfo.InvariantCulture).ToUniversalTime();
|
||||
RefreshedAfter = DateTime.ParseExact(refreshedAfter, DataTimeFormat, System.Globalization.CultureInfo.InvariantCulture).ToUniversalTime();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ExpiresAt = DateTime.Parse(expiresAt).ToUniversalTime();
|
||||
RefreshedAfter = DateTime.Parse(refreshedAfter).ToUniversalTime();
|
||||
}
|
||||
this.PublicKey = PublicKey;
|
||||
this.PrivateKey = PrivateKey;
|
||||
this.ExpiresAt = ExpiresAt;
|
||||
this.RefreshedAfter = RefreshedAfter;
|
||||
}
|
||||
|
||||
public bool NeedRefresh()
|
||||
|
|
@ -54,21 +57,6 @@ namespace MinecraftClient.Protocol.ProfileKey
|
|||
return timeOffset.ToUnixTimeSeconds();
|
||||
}
|
||||
|
||||
public static PlayerKeyPair FromString(string tokenString)
|
||||
{
|
||||
string[] fields = tokenString.Split(',');
|
||||
|
||||
if (fields.Length < 6)
|
||||
throw new InvalidDataException("Invalid string format");
|
||||
|
||||
PublicKey publicKey = new(pemKey: fields[0].Trim(),
|
||||
sig: fields[1].Trim(), sigV2: fields[2].Trim());
|
||||
|
||||
PrivateKey privateKey = new(pemKey: fields[3].Trim());
|
||||
|
||||
return new PlayerKeyPair(publicKey, privateKey, fields[4].Trim(), fields[5].Trim());
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
List<string> datas = new();
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json.Serialization;
|
||||
using MinecraftClient.Protocol.Message;
|
||||
|
||||
namespace MinecraftClient.Protocol.ProfileKey
|
||||
{
|
||||
public class PrivateKey
|
||||
{
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("Key")]
|
||||
public byte[] Key { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
private readonly RSA rsa;
|
||||
|
||||
[JsonIgnore]
|
||||
private byte[]? precedingSignature = null;
|
||||
|
||||
public PrivateKey(string pemKey)
|
||||
|
|
@ -20,6 +25,14 @@ namespace MinecraftClient.Protocol.ProfileKey
|
|||
rsa.ImportPkcs8PrivateKey(Key, out _);
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public PrivateKey(byte[] Key)
|
||||
{
|
||||
this.Key = Key;
|
||||
rsa = RSA.Create();
|
||||
rsa.ImportPkcs8PrivateKey(Key, out _);
|
||||
}
|
||||
|
||||
public byte[] SignData(byte[] data)
|
||||
{
|
||||
return rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,26 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json.Serialization;
|
||||
using MinecraftClient.Protocol.Message;
|
||||
|
||||
namespace MinecraftClient.Protocol.ProfileKey
|
||||
{
|
||||
public class PublicKey
|
||||
{
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("Key")]
|
||||
public byte[] Key { get; set; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("Signature")]
|
||||
public byte[]? Signature { get; set; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("SignatureV2")]
|
||||
public byte[]? SignatureV2 { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
private readonly RSA rsa;
|
||||
|
||||
public PublicKey(string pemKey, string? sig = null, string? sigV2 = null)
|
||||
|
|
@ -36,6 +47,12 @@ namespace MinecraftClient.Protocol.ProfileKey
|
|||
Signature = signature;
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public PublicKey(byte[] Key, byte[]? Signature, byte[]? SignatureV2) : this(Key, Signature!)
|
||||
{
|
||||
this.SignatureV2 = SignatureV2;
|
||||
}
|
||||
|
||||
public bool VerifyData(byte[] data, byte[] signature)
|
||||
{
|
||||
return rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue