diff --git a/Pal.Client/Configuration/AccountConfigurationV7.cs b/Pal.Client/Configuration/AccountConfigurationV7.cs
index dd994ca..5082463 100644
--- a/Pal.Client/Configuration/AccountConfigurationV7.cs
+++ b/Pal.Client/Configuration/AccountConfigurationV7.cs
@@ -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;
+
+ ///
+ /// 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(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 CachedRoles { get; set; } = new();
- private Guid? DecryptAccountId(string id)
+ private Guid? DecryptAccountId()
{
- if (Guid.TryParse(id, out Guid guid) && guid != Guid.Empty)
- return guid;
-
- if (!id.StartsWith("s:"))
- throw new InvalidOperationException("invalid prefix");
-
- try
+ if (Format == EFormat.UseProtectedData)
{
- byte[] guidBytes = ProtectedData.Unprotect(Convert.FromBase64String(id.Substring(2)),
- ConfigurationData.Entropy, DataProtectionScope.CurrentUser);
- return new Guid(guidBytes);
+ 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;
+ }
}
- catch (Exception e)
- {
- PluginLog.Verbose(e, $"Could not load account id {id}");
+ 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()))
+ {
+ 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,
+ }
}
}
diff --git a/Pal.Client/Configuration/ConfigurationData.cs b/Pal.Client/Configuration/ConfigurationData.cs
index 4131246..e8cb169 100644
--- a/Pal.Client/Configuration/ConfigurationData.cs
+++ b/Pal.Client/Configuration/ConfigurationData.cs
@@ -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 };
}
}
diff --git a/Pal.Client/Configuration/ConfigurationManager.cs b/Pal.Client/Configuration/ConfigurationManager.cs
index 9aabd3c..86757a7 100644
--- a/Pal.Client/Configuration/ConfigurationManager.cs
+++ b/Pal.Client/Configuration/ConfigurationManager.cs
@@ -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(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(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()
diff --git a/Pal.Client/Pal.Client.csproj b/Pal.Client/Pal.Client.csproj
index 96ae04d..fd6869a 100644
--- a/Pal.Client/Pal.Client.csproj
+++ b/Pal.Client/Pal.Client.csproj
@@ -14,6 +14,7 @@
Palace Pal
true
false
+ false
true