239 lines
8.4 KiB
C#
239 lines
8.4 KiB
C#
|
|
using System;
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using System.Collections.ObjectModel;
|
||
|
|
using System.Linq;
|
||
|
|
using System.Security.Cryptography.X509Certificates;
|
||
|
|
using System.Threading.Tasks;
|
||
|
|
using CommunityToolkit.Mvvm.Input;
|
||
|
|
using Wino.Core.Domain;
|
||
|
|
using Wino.Core.Domain.Enums;
|
||
|
|
using Wino.Core.Domain.Interfaces;
|
||
|
|
using Wino.Core.Domain.Models.Common;
|
||
|
|
|
||
|
|
namespace Wino.Mail.ViewModels;
|
||
|
|
|
||
|
|
public partial class SignatureAndEncryptionPageViewModel : MailBaseViewModel
|
||
|
|
{
|
||
|
|
private readonly ISmimeCertificateService _smimeCertificateService;
|
||
|
|
private readonly IDialogServiceBase _dialogService;
|
||
|
|
private readonly IFileService _fileService;
|
||
|
|
|
||
|
|
public ObservableCollection<X509Certificate2> PersonalCertificates { get; } = [];
|
||
|
|
public ObservableCollection<X509Certificate2> RecipientCertificates { get; } = [];
|
||
|
|
public List<X509Certificate2> SelectedPersonalCertificates { get; } = [];
|
||
|
|
public List<X509Certificate2> SelectedRecipientCertificates { get; } = [];
|
||
|
|
|
||
|
|
public bool PersonalCertificatesEmpty => PersonalCertificates.Count == 0;
|
||
|
|
|
||
|
|
public SignatureAndEncryptionPageViewModel(
|
||
|
|
IDialogServiceBase dialogService,
|
||
|
|
ISmimeCertificateService smimeCertificateService,
|
||
|
|
IFileService fileService
|
||
|
|
)
|
||
|
|
{
|
||
|
|
_dialogService = dialogService;
|
||
|
|
_fileService = fileService;
|
||
|
|
_smimeCertificateService = smimeCertificateService;
|
||
|
|
|
||
|
|
PersonalCertificates.CollectionChanged += (s, e) => { OnPropertyChanged(nameof(PersonalCertificatesEmpty)); };
|
||
|
|
LoadAllCertificates();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void LoadAllCertificates()
|
||
|
|
{
|
||
|
|
PersonalCertificates.Clear();
|
||
|
|
var personalCerts = _smimeCertificateService.GetCertificates();
|
||
|
|
foreach (var cert in personalCerts)
|
||
|
|
{
|
||
|
|
PersonalCertificates.Add(cert);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Recipient certificates
|
||
|
|
RecipientCertificates.Clear();
|
||
|
|
var recipientCerts = _smimeCertificateService.GetCertificates(storeName: StoreName.AddressBook);
|
||
|
|
foreach (var cert in recipientCerts)
|
||
|
|
{
|
||
|
|
RecipientCertificates.Add(cert);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
[RelayCommand]
|
||
|
|
public async Task ImportPersonalCertificatesAsync()
|
||
|
|
{
|
||
|
|
await ImportCertificates(StoreName.My);
|
||
|
|
}
|
||
|
|
|
||
|
|
[RelayCommand]
|
||
|
|
public async Task ImportRecipientCertificatesAsync()
|
||
|
|
{
|
||
|
|
await ImportCertificates(StoreName.AddressBook);
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task ImportCertificates(StoreName storeName)
|
||
|
|
{
|
||
|
|
var files = await PickCertificateFilesAsync();
|
||
|
|
var failedImports = new List<string>();
|
||
|
|
var successCount = 0;
|
||
|
|
foreach (var file in files)
|
||
|
|
{
|
||
|
|
string password = null;
|
||
|
|
if (file.FileExtension.Equals(".pfx") || file.FileExtension.Equals(".p12"))
|
||
|
|
{
|
||
|
|
password = await PromptForPasswordAsync(file.FileName);
|
||
|
|
}
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
_smimeCertificateService.ImportCertificate(file.FileExtension, file.Data, password,
|
||
|
|
storeName: storeName);
|
||
|
|
successCount++;
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
failedImports.Add($"{file.FileName}: {ex.Message}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
LoadAllCertificates();
|
||
|
|
if (successCount > 0)
|
||
|
|
{
|
||
|
|
_dialogService.InfoBarMessage(
|
||
|
|
string.Format(Translator.Smime_ImportCertificates_Success),
|
||
|
|
Translator.GeneralTitle_Info,
|
||
|
|
InfoBarMessageType.Success);
|
||
|
|
}
|
||
|
|
if (failedImports.Count > 0)
|
||
|
|
{
|
||
|
|
await _dialogService.ShowMessageAsync(
|
||
|
|
$"{Translator.Smime_ImportCertificates_Error}\n\n{string.Join("\n", failedImports)}",
|
||
|
|
Translator.GeneralTitle_Warning,
|
||
|
|
Core.Domain.Enums.WinoCustomMessageDialogIcon.Warning);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
[RelayCommand]
|
||
|
|
public async Task RemovePersonalCertificatesAsync()
|
||
|
|
{
|
||
|
|
await RemoveCertificatesAsync(SelectedPersonalCertificates, StoreName.My);
|
||
|
|
}
|
||
|
|
|
||
|
|
[RelayCommand]
|
||
|
|
public async Task RemoveRecipientCertificatesAsync()
|
||
|
|
{
|
||
|
|
await RemoveCertificatesAsync(SelectedRecipientCertificates, StoreName.AddressBook);
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task RemoveCertificatesAsync(List<X509Certificate2> certificates, StoreName storeName)
|
||
|
|
{
|
||
|
|
if (certificates.Any())
|
||
|
|
{
|
||
|
|
var confirm = await ConfirmAsync(string.Format(Translator.Smime_RemoveCertificates_Confirm,
|
||
|
|
string.Join(", ", certificates.Select(cert => cert.Subject))));
|
||
|
|
if (confirm)
|
||
|
|
{
|
||
|
|
foreach (var cert in certificates)
|
||
|
|
{
|
||
|
|
_smimeCertificateService.RemoveCertificate(cert.Thumbprint, storeName: storeName);
|
||
|
|
}
|
||
|
|
|
||
|
|
LoadAllCertificates();
|
||
|
|
_dialogService.InfoBarMessage(
|
||
|
|
Translator.Smime_RemoveCertificates_Success,
|
||
|
|
Translator.GeneralTitle_Info,
|
||
|
|
InfoBarMessageType.Success
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
[RelayCommand]
|
||
|
|
public async Task ExportPersonalCertificatesAsync()
|
||
|
|
{
|
||
|
|
await ExportCertificatesAsync(SelectedPersonalCertificates);
|
||
|
|
}
|
||
|
|
|
||
|
|
[RelayCommand]
|
||
|
|
public async Task ExportRecipientCertificatesAsync()
|
||
|
|
{
|
||
|
|
await ExportCertificatesAsync(SelectedRecipientCertificates);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Export logic for .cer or .pem
|
||
|
|
private async Task ExportCertificatesAsync(IEnumerable<X509Certificate2> cert)
|
||
|
|
{
|
||
|
|
var failedExports = new List<string>();
|
||
|
|
var successCount = 0;
|
||
|
|
foreach (var certificate in cert)
|
||
|
|
{
|
||
|
|
var fileName = $"{certificate.Subject.Replace("CN=", "")}.cer";
|
||
|
|
var path = await _dialogService.PickFilePathAsync(fileName);
|
||
|
|
if (path != null)
|
||
|
|
{
|
||
|
|
var folderPath = System.IO.Path.GetDirectoryName(path);
|
||
|
|
await using var stream = await _fileService.GetFileStreamAsync(folderPath, fileName);
|
||
|
|
if (stream != null)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var certificateData = certificate.Export(X509ContentType.Cert);
|
||
|
|
await stream.WriteAsync(certificateData, 0, certificateData.Length);
|
||
|
|
await stream.FlushAsync();
|
||
|
|
successCount++;
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
failedExports.Add($"{certificate.Subject}: {ex.Message}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
failedExports.Add($"{certificate.Subject}: File stream error");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (successCount > 0)
|
||
|
|
{
|
||
|
|
_dialogService.InfoBarMessage(
|
||
|
|
Translator.Smime_ExportCertificates_Success,
|
||
|
|
Translator.GeneralTitle_Info,
|
||
|
|
InfoBarMessageType.Success
|
||
|
|
);
|
||
|
|
}
|
||
|
|
if (failedExports.Count > 0)
|
||
|
|
{
|
||
|
|
await _dialogService.ShowMessageAsync(
|
||
|
|
$"{Translator.Smime_ExportCertificates_Error}\n\n{string.Join("\n", failedExports)}",
|
||
|
|
Translator.GeneralTitle_Warning,
|
||
|
|
Core.Domain.Enums.WinoCustomMessageDialogIcon.Warning);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task ShowCertificateDetailsAsync(X509Certificate2 cert)
|
||
|
|
{
|
||
|
|
var details = string.Format(Translator.Smime_CertificateDetails, cert.Subject, cert.Issuer, cert.NotBefore,
|
||
|
|
cert.NotAfter, cert.Thumbprint);
|
||
|
|
await _dialogService.ShowMessageAsync(details, Translator.GeneralTitle_Info,
|
||
|
|
Core.Domain.Enums.WinoCustomMessageDialogIcon.Information);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Confirmation dialog
|
||
|
|
private async Task<bool> ConfirmAsync(string message)
|
||
|
|
{
|
||
|
|
return await _dialogService.ShowConfirmationDialogAsync(message, Translator.Smime_Confirm_Title,
|
||
|
|
Translator.Buttons_Yes);
|
||
|
|
}
|
||
|
|
|
||
|
|
// File picker for importing certificates
|
||
|
|
private async Task<List<SharedFile>> PickCertificateFilesAsync()
|
||
|
|
{
|
||
|
|
return await _dialogService.PickFilesAsync(".pfx", ".p12", ".cer", ".crt");
|
||
|
|
}
|
||
|
|
|
||
|
|
// Ask for password for .pfx/.p12
|
||
|
|
private async Task<string> PromptForPasswordAsync(string fileName)
|
||
|
|
{
|
||
|
|
return await _dialogService.ShowTextInputDialogAsync("",
|
||
|
|
Translator.Smime_CertificatePassword_Title,
|
||
|
|
string.Format(Translator.Smime_CertificatePassword_Placeholder, fileName), Translator.Buttons_OK);
|
||
|
|
}
|
||
|
|
}
|