Config: Improve account model
This commit is contained in:
parent
16a17e0dcf
commit
550fa92a53
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json.Serialization;
|
||||
using Dalamud.Logging;
|
||||
@ -16,7 +17,7 @@ namespace Pal.Client.Configuration
|
||||
public AccountConfigurationV7(string server, Guid accountId)
|
||||
{
|
||||
Server = server;
|
||||
EncryptedId = EncryptAccountId(accountId);
|
||||
(EncryptedId, Entropy, Format) = EncryptAccountId(accountId);
|
||||
}
|
||||
|
||||
[Obsolete("for V1 import")]
|
||||
@ -25,69 +26,111 @@ namespace Pal.Client.Configuration
|
||||
Server = server;
|
||||
|
||||
if (accountId.StartsWith("s:"))
|
||||
EncryptedId = accountId;
|
||||
{
|
||||
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 = EncryptAccountId(guid);
|
||||
(EncryptedId, Entropy, Format) = EncryptAccountId(guid);
|
||||
else
|
||||
throw new InvalidOperationException("invalid account id format");
|
||||
throw new InvalidOperationException($"Invalid account id format, can't migrate account for server {server}");
|
||||
}
|
||||
|
||||
[JsonInclude]
|
||||
public EFormat Format { get; private set; } = EFormat.Unencrypted;
|
||||
|
||||
/// <summary>
|
||||
/// Depending on <see cref="Format"/>, this is either a Guid as string or a base64 encoded byte array.
|
||||
/// </summary>
|
||||
[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(EncryptedId) != null;
|
||||
[JsonIgnore] public bool IsUsable => DecryptAccountId() != null;
|
||||
|
||||
[JsonIgnore] public Guid AccountId => DecryptAccountId(EncryptedId) ?? throw new InvalidOperationException();
|
||||
[JsonIgnore] public Guid AccountId => DecryptAccountId() ?? throw new InvalidOperationException("Account id can't be read");
|
||||
|
||||
public List<string> CachedRoles { get; set; } = new();
|
||||
|
||||
private Guid? DecryptAccountId(string id)
|
||||
private Guid? DecryptAccountId()
|
||||
{
|
||||
if (Format == EFormat.UseProtectedData)
|
||||
{
|
||||
if (Guid.TryParse(id, out Guid guid) && guid != Guid.Empty)
|
||||
return guid;
|
||||
|
||||
if (!id.StartsWith("s:"))
|
||||
throw new InvalidOperationException("invalid prefix");
|
||||
|
||||
try
|
||||
{
|
||||
byte[] guidBytes = ProtectedData.Unprotect(Convert.FromBase64String(id.Substring(2)),
|
||||
ConfigurationData.Entropy, DataProtectionScope.CurrentUser);
|
||||
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 {id}");
|
||||
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 EncryptAccountId(Guid g)
|
||||
private (string encryptedId, byte[]? entropy, EFormat format) EncryptAccountId(Guid g)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] guidBytes = ProtectedData.Protect(g.ToByteArray(), ConfigurationData.Entropy,
|
||||
DataProtectionScope.CurrentUser);
|
||||
return $"s:{Convert.ToBase64String(guidBytes)}";
|
||||
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();
|
||||
return (g.ToString(), null, EFormat.Unencrypted);
|
||||
}
|
||||
}
|
||||
|
||||
public bool EncryptIfNeeded()
|
||||
{
|
||||
if (Guid.TryParse(EncryptedId, out Guid g))
|
||||
if (Format == EFormat.Unencrypted)
|
||||
{
|
||||
string oldId = EncryptedId;
|
||||
EncryptedId = EncryptAccountId(g);
|
||||
return oldId != EncryptedId;
|
||||
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<byte>()))
|
||||
{
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
namespace Pal.Client.Configuration
|
||||
using System;
|
||||
|
||||
namespace Pal.Client.Configuration
|
||||
{
|
||||
internal static class ConfigurationData
|
||||
{
|
||||
internal static readonly byte[] Entropy = { 0x22, 0x4b, 0xe7, 0x21, 0x44, 0x83, 0x69, 0x55, 0x80, 0x38 };
|
||||
[Obsolete("for V1 import")]
|
||||
internal static readonly byte[] FixedV1Entropy = { 0x22, 0x4b, 0xe7, 0x21, 0x44, 0x83, 0x69, 0x55, 0x80, 0x38 };
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,19 @@ namespace Pal.Client.Configuration
|
||||
|
||||
public string ConfigPath => Path.Join(_pluginInterface.GetPluginConfigDirectory(), "palace-pal.config.json");
|
||||
|
||||
public IPalacePalConfiguration Load()
|
||||
{
|
||||
return JsonSerializer.Deserialize<ConfigurationV7>(File.ReadAllText(ConfigPath, Encoding.UTF8)) ??
|
||||
new ConfigurationV7();
|
||||
}
|
||||
|
||||
public void Save(IConfigurationInConfigDirectory config)
|
||||
{
|
||||
File.WriteAllText(ConfigPath,
|
||||
JsonSerializer.Serialize(config, config.GetType(), new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }),
|
||||
Encoding.UTF8);
|
||||
}
|
||||
|
||||
#pragma warning disable CS0612
|
||||
#pragma warning disable CS0618
|
||||
public void Migrate()
|
||||
@ -42,19 +55,6 @@ namespace Pal.Client.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
public IPalacePalConfiguration Load()
|
||||
{
|
||||
return JsonSerializer.Deserialize<ConfigurationV7>(File.ReadAllText(ConfigPath, Encoding.UTF8)) ??
|
||||
new ConfigurationV7();
|
||||
}
|
||||
|
||||
public void Save(IConfigurationInConfigDirectory config)
|
||||
{
|
||||
File.WriteAllText(ConfigPath,
|
||||
JsonSerializer.Serialize(config, config.GetType(), new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }),
|
||||
Encoding.UTF8);
|
||||
}
|
||||
|
||||
private ConfigurationV7 MigrateToV7(ConfigurationV1 v1)
|
||||
{
|
||||
ConfigurationV7 v7 = new()
|
||||
|
@ -14,6 +14,7 @@
|
||||
<AssemblyName>Palace Pal</AssemblyName>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user