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,
}
}
}