2023-02-15 01:38:04 +00:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Security.Cryptography;
|
|
|
|
|
using System.Text.Json.Serialization;
|
2023-02-16 23:54:23 +00:00
|
|
|
|
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
|
|
|
|
|
2023-02-16 23:54:23 +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>
|
2023-02-15 09:20:25 +00:00
|
|
|
|
[JsonPropertyName("Id")]
|
|
|
|
|
[JsonInclude]
|
2023-02-15 13:35:11 +00:00
|
|
|
|
[JsonRequired]
|
2023-02-15 09:20:25 +00:00
|
|
|
|
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]
|
2023-02-15 09:20:25 +00:00
|
|
|
|
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)
|
|
|
|
|
{
|
2023-02-16 23:54:23 +00:00
|
|
|
|
_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
|
|
|
|
|
2023-02-15 09:20:25 +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 09:20:25 +00:00
|
|
|
|
{
|
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 09:20:25 +00:00
|
|
|
|
}
|
2023-02-15 12:00:00 +00:00
|
|
|
|
|
2023-02-15 09:20:25 +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
|
|
|
|
}
|
|
|
|
|
}
|