using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text.Json.Serialization; using Dalamud.Logging; namespace Pal.Client.Configuration { public class AccountConfigurationV7 : IAccountConfiguration { [JsonConstructor] public AccountConfigurationV7() { } public AccountConfigurationV7(string server, Guid accountId) { Server = server; (EncryptedId, Entropy, Format) = EncryptAccountId(accountId); } [Obsolete("for V1 import")] public AccountConfigurationV7(string server, string accountId) { Server = server; if (accountId.StartsWith("s:")) { EncryptedId = accountId.Substring(2); Entropy = ConfigurationData.FixedV1Entropy; Format = EFormat.UseProtectedData; // try to migrate away from v1 entropy if possible Guid? decryptedId = DecryptAccountId(); if (decryptedId != null) (EncryptedId, Entropy, Format) = EncryptAccountId(decryptedId.Value); } else if (Guid.TryParse(accountId, out Guid guid)) (EncryptedId, Entropy, Format) = EncryptAccountId(guid); else throw new InvalidOperationException($"Invalid account id format, can't migrate account for server {server}"); } [JsonInclude] public EFormat Format { get; private set; } = EFormat.Unencrypted; /// /// Depending on , this is either a Guid as string or a base64 encoded byte array. /// [JsonPropertyName("Id")] [JsonInclude] public string EncryptedId { get; private set; } = null!; [JsonInclude] public byte[]? Entropy { get; private set; } public string Server { get; init; } = null!; [JsonIgnore] public bool IsUsable => DecryptAccountId() != null; [JsonIgnore] public Guid AccountId => DecryptAccountId() ?? throw new InvalidOperationException("Account id can't be read"); public List CachedRoles { get; set; } = new(); private Guid? DecryptAccountId() { if (Format == EFormat.UseProtectedData) { try { byte[] guidBytes = ProtectedData.Unprotect(Convert.FromBase64String(EncryptedId), Entropy, DataProtectionScope.CurrentUser); return new Guid(guidBytes); } catch (Exception e) { PluginLog.Verbose(e, $"Could not load account id {EncryptedId}"); return null; } } else if (Format == EFormat.Unencrypted) return Guid.Parse(EncryptedId); else return null; } private (string encryptedId, byte[]? entropy, EFormat format) EncryptAccountId(Guid g) { try { byte[] entropy = RandomNumberGenerator.GetBytes(16); byte[] guidBytes = ProtectedData.Protect(g.ToByteArray(), entropy, DataProtectionScope.CurrentUser); return (Convert.ToBase64String(guidBytes), entropy, EFormat.UseProtectedData); } catch (Exception) { return (g.ToString(), null, EFormat.Unencrypted); } } public bool EncryptIfNeeded() { if (Format == EFormat.Unencrypted) { var (newId, newEntropy, newFormat) = EncryptAccountId(Guid.Parse(EncryptedId)); if (newFormat != EFormat.Unencrypted) { EncryptedId = newId; Entropy = newEntropy; Format = newFormat; return true; } } #pragma warning disable CS0618 // Type or member is obsolete if (Format == EFormat.UseProtectedData && ConfigurationData.FixedV1Entropy.SequenceEqual(Entropy ?? Array.Empty())) { Guid? g = DecryptAccountId(); if (g != null) { (EncryptedId, Entropy, Format) = EncryptAccountId(g.Value); return true; } } #pragma warning restore CS0618 // Type or member is obsolete return false; } public enum EFormat { Unencrypted = 1, UseProtectedData = 2, } } }