Refactoring to asynchronous. (partially completed)

This commit is contained in:
BruceChen 2022-12-20 22:41:14 +08:00
parent 7ee08092d4
commit 096ea0c70c
72 changed files with 6033 additions and 5080 deletions

View file

@ -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);

View file

@ -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));
}
}
}
}

View file

@ -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();

View file

@ -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);

View file

@ -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);