Files
OpenRA/OpenRA.Game/LocalPlayerProfile.cs
RoosterDragon b58c1ea5bc Provide names and pools when creating MiniYaml.
- Rename the filename parameter to name and make it mandatory. Review all callers and ensure a useful string is provided as input, to ensure sufficient context is included for logging and debugging. This can be a filename, url, or any arbitrary text so include whatever context seems reasonable.
- When several MiniYamls are created that have similar content, provide a shared string pool. This allows strings that are common between all the yaml to be shared, reducing long term memory usage. We also change the pool from a dictionary to a set. Originally a Dictionary had to be used so we could call TryGetValue to get a reference to the pooled string. Now that more recent versions of dotnet provide a TryGetValue on HashSet, we can use a set directly without the memory wasted by having to store both keys and values in a dictionary.
2024-01-21 12:39:10 +02:00

194 lines
5.2 KiB
C#

#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using OpenRA.Support;
namespace OpenRA
{
public sealed class LocalPlayerProfile
{
const int AuthKeySize = 2048;
public enum LinkState { Uninitialized, GeneratingKeys, Unlinked, CheckingLink, ConnectionFailed, Linked }
public LinkState State => innerState;
public string Fingerprint => innerFingerprint;
public string PublicKey => innerPublicKey;
public PlayerProfile ProfileData => innerData;
volatile LinkState innerState;
volatile PlayerProfile innerData;
volatile string innerFingerprint;
volatile string innerPublicKey;
RSAParameters parameters;
readonly string filePath;
readonly PlayerDatabase playerDatabase;
public LocalPlayerProfile(string filePath, PlayerDatabase playerDatabase)
{
this.filePath = filePath;
this.playerDatabase = playerDatabase;
innerState = LinkState.Uninitialized;
try
{
if (File.Exists(filePath))
{
using (var rsa = new RSACryptoServiceProvider())
{
using (var data = File.OpenRead(filePath))
{
var keyData = Convert.FromBase64String(data.ReadAllText());
rsa.FromXmlString(new string(Encoding.ASCII.GetChars(keyData)));
}
parameters = rsa.ExportParameters(true);
innerPublicKey = CryptoUtil.EncodePEMPublicKey(parameters);
innerFingerprint = CryptoUtil.PublicKeyFingerprint(parameters);
innerState = LinkState.Unlinked;
}
}
}
catch (Exception e)
{
Console.WriteLine("Failed to load keys:");
Console.WriteLine(e);
Log.Write("debug", $"Failed to load player keypair from `{filePath}` with exception:");
Log.Write("debug", e);
}
}
public void RefreshPlayerData(Action onComplete = null)
{
if (State != LinkState.Unlinked && State != LinkState.Linked && State != LinkState.ConnectionFailed)
return;
Task.Run(async () =>
{
try
{
var client = HttpClientFactory.Create();
var url = playerDatabase.Profile + Fingerprint;
var httpResponseMessage = await client.GetAsync(url);
var result = await httpResponseMessage.Content.ReadAsStreamAsync();
var yaml = MiniYaml.FromStream(result, url).First();
if (yaml.Key == "Player")
{
innerData = FieldLoader.Load<PlayerProfile>(yaml.Value);
if (innerData.KeyRevoked)
{
Log.Write("debug", $"Revoking key with fingerprint {Fingerprint}");
DeleteKeypair();
}
else
innerState = LinkState.Linked;
}
else
innerState = LinkState.Unlinked;
}
catch (Exception e)
{
Log.Write("debug", "Failed to parse player data result with exception:");
Log.Write("debug", e);
innerState = LinkState.ConnectionFailed;
}
finally
{
onComplete?.Invoke();
}
});
innerState = LinkState.CheckingLink;
}
public void GenerateKeypair()
{
if (State != LinkState.Uninitialized)
return;
innerState = LinkState.GeneratingKeys;
new Task(() =>
{
try
{
var rsa = new RSACryptoServiceProvider(AuthKeySize);
parameters = rsa.ExportParameters(true);
innerPublicKey = CryptoUtil.EncodePEMPublicKey(parameters);
innerFingerprint = CryptoUtil.PublicKeyFingerprint(parameters);
var data = Convert.ToBase64String(Encoding.ASCII.GetBytes(rsa.ToXmlString(true)));
File.WriteAllText(filePath, data);
innerState = LinkState.Unlinked;
}
catch (Exception e)
{
Log.Write("debug", "Failed to generate keypair with exception:");
Log.Write("debug", e);
Console.WriteLine("Key generation failed:");
Console.WriteLine(e);
innerState = LinkState.Uninitialized;
}
}).Start();
}
public void DeleteKeypair()
{
try
{
File.Delete(filePath);
}
catch (Exception e)
{
Log.Write("debug", "Failed to delete keypair with exception:");
Log.Write("debug", e);
Console.WriteLine("Key deletion failed:");
Console.WriteLine(e);
}
innerState = LinkState.Uninitialized;
parameters = default;
innerFingerprint = null;
innerData = null;
}
public string Sign(params string[] data)
{
// If we don't have any keys, or we know for sure that they haven't been linked to the forum
// then we can't do much here. If we have keys but don't yet know if they have been linked to the
// forum (LinkState.CheckingLink or ConnectionFailed) then we sign to avoid blocking the main thread
// but accept that - if the cert is invalid - the server will reject the result.
if (State <= LinkState.Unlinked)
return null;
return CryptoUtil.Sign(parameters, data.Where(x => !string.IsNullOrEmpty(x)).JoinWith(string.Empty));
}
public string DecryptString(string data)
{
if (State <= LinkState.Unlinked)
return null;
return CryptoUtil.DecryptString(parameters, data);
}
}
}