- 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.
194 lines
5.2 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|