#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 httpResponseMessage = await client.GetAsync(playerDatabase.Profile + Fingerprint); var result = await httpResponseMessage.Content.ReadAsStreamAsync(); var yaml = MiniYaml.FromStream(result).First(); if (yaml.Key == "Player") { innerData = FieldLoader.Load(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); } } }