Update UI & Syncshell Public & MCDF Share

This commit is contained in:
2025-11-01 19:55:49 +01:00
parent 513845b811
commit 8cc4f34c55
22 changed files with 1949 additions and 298 deletions

View File

@@ -0,0 +1,309 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using MareSynchronos.API.Dto.McdfShare;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services.CharaData;
public sealed class McdfShareManager
{
private readonly ILogger<McdfShareManager> _logger;
private readonly ApiController _apiController;
private readonly CharaDataFileHandler _fileHandler;
private readonly CharaDataManager _charaDataManager;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly SemaphoreSlim _operationSemaphore = new(1, 1);
private readonly List<McdfShareEntryDto> _ownShares = new();
private readonly List<McdfShareEntryDto> _sharedWithMe = new();
private Task? _currentTask;
public McdfShareManager(ILogger<McdfShareManager> logger, ApiController apiController,
CharaDataFileHandler fileHandler, CharaDataManager charaDataManager,
ServerConfigurationManager serverConfigurationManager)
{
_logger = logger;
_apiController = apiController;
_fileHandler = fileHandler;
_charaDataManager = charaDataManager;
_serverConfigurationManager = serverConfigurationManager;
}
public IReadOnlyList<McdfShareEntryDto> OwnShares => _ownShares;
public IReadOnlyList<McdfShareEntryDto> SharedShares => _sharedWithMe;
public bool IsBusy => _currentTask is { IsCompleted: false };
public string? LastError { get; private set; }
public string? LastSuccess { get; private set; }
public Task RefreshAsync(CancellationToken token)
{
return RunOperation(() => InternalRefreshAsync(token));
}
public Task CreateShareAsync(string description, IReadOnlyList<string> allowedIndividuals, IReadOnlyList<string> allowedSyncshells, DateTime? expiresAtUtc, CancellationToken token)
{
return RunOperation(async () =>
{
token.ThrowIfCancellationRequested();
var mcdfBytes = await _fileHandler.CreateCharaFileBytesAsync(description, token).ConfigureAwait(false);
if (mcdfBytes == null || mcdfBytes.Length == 0)
{
LastError = "Impossible de préparer les données MCDF.";
return;
}
var secretKey = _serverConfigurationManager.GetSecretKey(out bool hasMultiple);
if (hasMultiple)
{
LastError = "Plusieurs clés secrètes sont configurées pour ce personnage. Corrigez cela dans les paramètres.";
return;
}
if (string.IsNullOrEmpty(secretKey))
{
LastError = "Aucune clé secrète n'est configurée pour ce personnage.";
return;
}
var shareId = Guid.NewGuid();
byte[] salt = RandomNumberGenerator.GetBytes(16);
byte[] nonce = RandomNumberGenerator.GetBytes(12);
byte[] key = DeriveKey(secretKey, shareId, salt);
byte[] cipher = new byte[mcdfBytes.Length];
byte[] tag = new byte[16];
using (var aes = new AesGcm(key, 16))
{
aes.Encrypt(nonce, mcdfBytes, cipher, tag);
}
var uploadDto = new McdfShareUploadRequestDto
{
ShareId = shareId,
Description = description,
CipherData = cipher,
Nonce = nonce,
Salt = salt,
Tag = tag,
ExpiresAtUtc = expiresAtUtc,
AllowedIndividuals = allowedIndividuals.ToList(),
AllowedSyncshells = allowedSyncshells.ToList()
};
await _apiController.McdfShareUpload(uploadDto).ConfigureAwait(false);
await InternalRefreshAsync(token).ConfigureAwait(false);
LastSuccess = "Partage MCDF créé.";
});
}
public Task DeleteShareAsync(Guid shareId)
{
return RunOperation(async () =>
{
var result = await _apiController.McdfShareDelete(shareId).ConfigureAwait(false);
if (!result)
{
LastError = "Le serveur a refusé de supprimer le partage MCDF.";
return;
}
_ownShares.RemoveAll(s => s.Id == shareId);
_sharedWithMe.RemoveAll(s => s.Id == shareId);
await InternalRefreshAsync(CancellationToken.None).ConfigureAwait(false);
LastSuccess = "Partage MCDF supprimé.";
});
}
public Task UpdateShareAsync(McdfShareUpdateRequestDto updateRequest)
{
return RunOperation(async () =>
{
var updated = await _apiController.McdfShareUpdate(updateRequest).ConfigureAwait(false);
if (updated == null)
{
LastError = "Le serveur a refusé de mettre à jour le partage MCDF.";
return;
}
var idx = _ownShares.FindIndex(s => s.Id == updated.Id);
if (idx >= 0)
{
_ownShares[idx] = updated;
}
LastSuccess = "Partage MCDF mis à jour.";
});
}
public Task ApplyShareAsync(Guid shareId, CancellationToken token)
{
return RunOperation(async () =>
{
token.ThrowIfCancellationRequested();
var plainBytes = await DownloadAndDecryptShareAsync(shareId, token).ConfigureAwait(false);
if (plainBytes == null)
{
LastError ??= "Échec du téléchargement du partage MCDF.";
return;
}
var tempPath = await _charaDataManager.LoadMcdfFromBytes(plainBytes, token).ConfigureAwait(false);
try
{
await _charaDataManager.McdfApplyToGposeTarget().ConfigureAwait(false);
}
finally
{
try
{
if (File.Exists(tempPath))
{
File.Delete(tempPath);
}
}
catch
{
// ignored
}
}
LastSuccess = "Partage MCDF appliqué sur la cible GPose.";
});
}
public Task ExportShareAsync(Guid shareId, string filePath, CancellationToken token)
{
return RunOperation(async () =>
{
token.ThrowIfCancellationRequested();
var plainBytes = await DownloadAndDecryptShareAsync(shareId, token).ConfigureAwait(false);
if (plainBytes == null)
{
LastError ??= "Échec du téléchargement du partage MCDF.";
return;
}
var directory = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
await File.WriteAllBytesAsync(filePath, plainBytes, token).ConfigureAwait(false);
LastSuccess = "Partage MCDF exporté.";
});
}
public Task DownloadShareToFileAsync(McdfShareEntryDto entry, string filePath, CancellationToken token)
{
return ExportShareAsync(entry.Id, filePath, token);
}
private async Task<byte[]?> DownloadAndDecryptShareAsync(Guid shareId, CancellationToken token)
{
var payload = await _apiController.McdfShareDownload(shareId).ConfigureAwait(false);
if (payload == null)
{
LastError = "Partage indisponible.";
return null;
}
var secretKey = _serverConfigurationManager.GetSecretKey(out bool hasMultiple);
if (hasMultiple)
{
LastError = "Plusieurs clés secrètes sont configurées pour ce personnage.";
return null;
}
if (string.IsNullOrEmpty(secretKey))
{
LastError = "Aucune clé secrète n'est configurée pour ce personnage.";
return null;
}
byte[] key = DeriveKey(secretKey, payload.ShareId, payload.Salt);
byte[] plaintext = new byte[payload.CipherData.Length];
try
{
using var aes = new AesGcm(key, 16);
aes.Decrypt(payload.Nonce, payload.CipherData, payload.Tag, plaintext);
}
catch (CryptographicException ex)
{
_logger.LogWarning(ex, "Failed to decrypt MCDF share {ShareId}", shareId);
LastError = "Impossible de déchiffrer le partage MCDF.";
return null;
}
token.ThrowIfCancellationRequested();
return plaintext;
}
private async Task InternalRefreshAsync(CancellationToken token)
{
token.ThrowIfCancellationRequested();
var own = await _apiController.McdfShareGetOwn().ConfigureAwait(false);
token.ThrowIfCancellationRequested();
var shared = await _apiController.McdfShareGetShared().ConfigureAwait(false);
_ownShares.Clear();
_ownShares.AddRange(own);
_sharedWithMe.Clear();
_sharedWithMe.AddRange(shared);
LastSuccess = "Partages MCDF actualisés.";
}
private Task RunOperation(Func<Task> operation)
{
async Task Wrapper()
{
await _operationSemaphore.WaitAsync().ConfigureAwait(false);
try
{
LastError = null;
LastSuccess = null;
await operation().ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during MCDF share operation");
LastError = ex.Message;
}
finally
{
_operationSemaphore.Release();
}
}
var task = Wrapper();
_currentTask = task;
return task;
}
private static byte[] DeriveKey(string secretKey, Guid shareId, byte[] salt)
{
byte[] secretBytes;
try
{
secretBytes = Convert.FromHexString(secretKey);
}
catch (FormatException)
{
// fallback to UTF8 if not hex
secretBytes = System.Text.Encoding.UTF8.GetBytes(secretKey);
}
byte[] shareBytes = shareId.ToByteArray();
byte[] material = new byte[secretBytes.Length + shareBytes.Length + salt.Length];
Buffer.BlockCopy(secretBytes, 0, material, 0, secretBytes.Length);
Buffer.BlockCopy(shareBytes, 0, material, secretBytes.Length, shareBytes.Length);
Buffer.BlockCopy(salt, 0, material, secretBytes.Length + shareBytes.Length, salt.Length);
return SHA256.HashData(material);
}
}