PalacePal/Pal.Client/Configuration/AccountConfigurationV7.cs

150 lines
5.1 KiB
C#
Raw Normal View History

2023-02-15 01:38:04 +00:00
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
2023-02-15 01:38:04 +00:00
namespace Pal.Client.Configuration
{
2023-02-15 22:51:35 +00:00
public sealed class AccountConfigurationV7 : IAccountConfiguration
2023-02-15 01:38:04 +00:00
{
2023-02-15 12:34:44 +00:00
private const int DefaultEntropyLength = 16;
2023-02-15 12:27:41 +00:00
private static readonly ILogger _logger =
DependencyInjectionContext.LoggerProvider.CreateLogger<AccountConfigurationV7>();
2023-02-15 01:38:04 +00:00
[JsonConstructor]
public AccountConfigurationV7()
{
}
public AccountConfigurationV7(string server, Guid accountId)
{
Server = server;
2023-02-15 12:00:00 +00:00
(EncryptedId, Entropy, Format) = EncryptAccountId(accountId);
2023-02-15 01:38:04 +00:00
}
[Obsolete("for V1 import")]
public AccountConfigurationV7(string server, string accountId)
{
Server = server;
if (accountId.StartsWith("s:"))
2023-02-15 12:00:00 +00:00
{
EncryptedId = accountId.Substring(2);
Entropy = ConfigurationData.FixedV1Entropy;
Format = EFormat.UseProtectedData;
2023-02-15 12:27:41 +00:00
EncryptIfNeeded();
2023-02-15 12:00:00 +00:00
}
2023-02-15 01:38:04 +00:00
else if (Guid.TryParse(accountId, out Guid guid))
2023-02-15 12:00:00 +00:00
(EncryptedId, Entropy, Format) = EncryptAccountId(guid);
2023-02-15 01:38:04 +00:00
else
2023-02-15 12:00:00 +00:00
throw new InvalidOperationException($"Invalid account id format, can't migrate account for server {server}");
2023-02-15 01:38:04 +00:00
}
2023-02-15 12:00:00 +00:00
[JsonInclude]
2023-02-15 13:35:11 +00:00
[JsonRequired]
2023-02-15 12:00:00 +00:00
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]
2023-02-15 13:35:11 +00:00
[JsonRequired]
public string EncryptedId { get; private set; } = null!;
2023-02-15 01:38:04 +00:00
2023-02-15 12:00:00 +00:00
[JsonInclude]
public byte[]? Entropy { get; private set; }
2023-02-15 13:35:11 +00:00
[JsonRequired]
public string Server { get; init; } = null!;
2023-02-15 01:38:04 +00:00
2023-02-15 12:00:00 +00:00
[JsonIgnore] public bool IsUsable => DecryptAccountId() != null;
2023-02-15 01:38:04 +00:00
2023-02-15 12:00:00 +00:00
[JsonIgnore] public Guid AccountId => DecryptAccountId() ?? throw new InvalidOperationException("Account id can't be read");
2023-02-15 01:38:04 +00:00
public List<string> CachedRoles { get; set; } = new();
2023-02-15 12:00:00 +00:00
private Guid? DecryptAccountId()
2023-02-15 01:38:04 +00:00
{
2023-02-15 12:27:41 +00:00
if (Format == EFormat.UseProtectedData && ConfigurationData.SupportsDpapi)
2023-02-15 01:38:04 +00:00
{
2023-02-15 12:00:00 +00:00
try
{
byte[] guidBytes = ProtectedData.Unprotect(Convert.FromBase64String(EncryptedId), Entropy, DataProtectionScope.CurrentUser);
return new Guid(guidBytes);
}
catch (Exception e)
{
_logger.LogTrace(e, "Could not load account id {Id}", EncryptedId);
2023-02-15 12:00:00 +00:00
return null;
}
2023-02-15 01:38:04 +00:00
}
2023-02-15 12:00:00 +00:00
else if (Format == EFormat.Unencrypted)
return Guid.Parse(EncryptedId);
2023-02-15 12:27:41 +00:00
else if (Format == EFormat.ProtectedDataUnsupported && !ConfigurationData.SupportsDpapi)
return Guid.Parse(EncryptedId);
2023-02-15 12:00:00 +00:00
else
2023-02-15 01:38:04 +00:00
return null;
}
2023-02-15 12:00:00 +00:00
private (string encryptedId, byte[]? entropy, EFormat format) EncryptAccountId(Guid g)
2023-02-15 01:38:04 +00:00
{
2023-02-15 12:27:41 +00:00
if (!ConfigurationData.SupportsDpapi)
return (g.ToString(), null, EFormat.ProtectedDataUnsupported);
else
2023-02-15 01:38:04 +00:00
{
2023-02-15 12:27:41 +00:00
try
{
2023-02-15 12:34:44 +00:00
byte[] entropy = RandomNumberGenerator.GetBytes(DefaultEntropyLength);
2023-02-15 12:27:41 +00:00
byte[] guidBytes = ProtectedData.Protect(g.ToByteArray(), entropy, DataProtectionScope.CurrentUser);
return (Convert.ToBase64String(guidBytes), entropy, EFormat.UseProtectedData);
}
catch (Exception)
{
return (g.ToString(), null, EFormat.Unencrypted);
}
2023-02-15 01:38:04 +00:00
}
}
2023-02-15 12:27:41 +00:00
public bool EncryptIfNeeded()
{
2023-02-15 12:00:00 +00:00
if (Format == EFormat.Unencrypted)
{
var (newId, newEntropy, newFormat) = EncryptAccountId(Guid.Parse(EncryptedId));
if (newFormat != EFormat.Unencrypted)
{
EncryptedId = newId;
Entropy = newEntropy;
Format = newFormat;
return true;
}
}
2023-02-15 12:34:44 +00:00
else if (Format == EFormat.UseProtectedData && Entropy is { Length: < DefaultEntropyLength })
{
2023-02-15 12:00:00 +00:00
Guid? g = DecryptAccountId();
if (g != null)
{
(EncryptedId, Entropy, Format) = EncryptAccountId(g.Value);
return true;
}
}
2023-02-15 12:00:00 +00:00
return false;
}
2023-02-15 12:00:00 +00:00
public enum EFormat
{
Unencrypted = 1,
UseProtectedData = 2,
2023-02-15 12:27:41 +00:00
/// <summary>
/// Used for filtering: We don't want to overwrite any entries of this value using DPAPI, ever.
/// This is mostly a wine fallback.
/// </summary>
ProtectedDataUnsupported = 3,
2023-02-15 12:00:00 +00:00
}
2023-02-15 01:38:04 +00:00
}
}