mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-06 22:36:27 -04:00
Added working client side decryption of emails (#117)
This commit is contained in:
76
src/AliasVault.Api/Controllers/EmailBoxController.cs
Normal file
76
src/AliasVault.Api/Controllers/EmailBoxController.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="EmailBoxController.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Api.Controllers;
|
||||
|
||||
using AliasServerDb;
|
||||
using AliasVault.Api.Helpers;
|
||||
using AliasVault.Shared.Models.Spamok;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// Email controller for retrieving emails from the database.
|
||||
/// </summary>
|
||||
/// <param name="dbContextFactory">DbContext instance.</param>
|
||||
/// <param name="userManager">UserManager instance.</param>
|
||||
[ApiVersion("1")]
|
||||
public class EmailBoxController(IDbContextFactory<AliasServerDbContext> dbContextFactory, UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the newest version of the vault for the current user.
|
||||
/// </summary>
|
||||
/// <param name="to">The full email address including @ sign.</param>
|
||||
/// <returns>List of aliases in JSON format.</returns>
|
||||
[HttpGet(template: "{to}", Name = "GetEmailBox")]
|
||||
public async Task<IActionResult> GetEmailBox(string to)
|
||||
{
|
||||
await using var context = await dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
var user = await GetCurrentUserAsync();
|
||||
if (user is null)
|
||||
{
|
||||
return Unauthorized("Not authenticated.");
|
||||
}
|
||||
|
||||
// See if this user has a valid claim to the email address.
|
||||
var emailClaim = await context.UserEmailClaims
|
||||
.FirstOrDefaultAsync(x => x.UserId == user.Id && x.Address == to);
|
||||
|
||||
if (emailClaim is null)
|
||||
{
|
||||
return Unauthorized("User does not have a claim to this email address.");
|
||||
}
|
||||
|
||||
// Retrieve emails from database.
|
||||
List<MailboxEmailApiModel> emails = context.Emails.AsNoTracking().Select(x => new MailboxEmailApiModel()
|
||||
{
|
||||
Id = x.Id,
|
||||
Subject = x.Subject,
|
||||
FromDisplay = ConversionHelper.ConvertFromToFromDisplay(x.From),
|
||||
FromDomain = x.FromDomain,
|
||||
FromLocal = x.FromLocal,
|
||||
ToDomain = x.ToDomain,
|
||||
ToLocal = x.ToLocal,
|
||||
Date = x.Date,
|
||||
DateSystem = x.DateSystem,
|
||||
SecondsAgo = (int)DateTime.UtcNow.Subtract(x.DateSystem).TotalSeconds,
|
||||
MessagePreview = x.MessagePreview ?? string.Empty,
|
||||
EncryptedSymmetricKey = x.EncryptedSymmetricKey,
|
||||
EncryptionKey = x.EncryptionKey.PublicKey,
|
||||
}).OrderByDescending(x => x.DateSystem).Take(75).ToList();
|
||||
|
||||
MailboxApiModel returnValue = new MailboxApiModel();
|
||||
returnValue.Address = to;
|
||||
returnValue.Subscribed = false;
|
||||
returnValue.Mails = emails;
|
||||
|
||||
return Ok(returnValue);
|
||||
}
|
||||
}
|
||||
96
src/AliasVault.Api/Controllers/EmailController.cs
Normal file
96
src/AliasVault.Api/Controllers/EmailController.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="EmailController.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Api.Controllers;
|
||||
|
||||
using AliasServerDb;
|
||||
using AliasVault.Api.Helpers;
|
||||
using AliasVault.Shared.Models.Spamok;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// Email controller for retrieving emails from the database.
|
||||
/// </summary>
|
||||
/// <param name="dbContextFactory">DbContext instance.</param>
|
||||
/// <param name="userManager">UserManager instance.</param>
|
||||
[ApiVersion("1")]
|
||||
public class EmailController(IDbContextFactory<AliasServerDbContext> dbContextFactory, UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the newest version of the vault for the current user.
|
||||
/// </summary>
|
||||
/// <param name="id">The email ID to open.</param>
|
||||
/// <returns>List of aliases in JSON format.</returns>
|
||||
[HttpGet(template: "{id}", Name = "GetEmail")]
|
||||
public async Task<IActionResult> GetEmail(int id)
|
||||
{
|
||||
await using var context = await dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
var user = await GetCurrentUserAsync();
|
||||
if (user is null)
|
||||
{
|
||||
return Unauthorized("Not authenticated.");
|
||||
}
|
||||
|
||||
// Retrieve email from database.
|
||||
var email = await context.Emails.Include(x => x.EncryptionKey).AsNoTracking().FirstOrDefaultAsync(x => x.Id == id);
|
||||
if (email is null)
|
||||
{
|
||||
return NotFound("Email not found.");
|
||||
}
|
||||
|
||||
// See if this user has a valid claim to the email address.
|
||||
var emailClaim = await context.UserEmailClaims
|
||||
.FirstOrDefaultAsync(x => x.UserId == user.Id && x.Address == email.To);
|
||||
|
||||
if (emailClaim is null)
|
||||
{
|
||||
return Unauthorized("User does not have a claim to this email address.");
|
||||
}
|
||||
|
||||
var returnEmail = new EmailApiModel
|
||||
{
|
||||
Id = email.Id,
|
||||
Subject = email.Subject,
|
||||
FromDisplay = ConversionHelper.ConvertFromToFromDisplay(email.From),
|
||||
FromDomain = email.FromDomain,
|
||||
FromLocal = email.FromLocal,
|
||||
ToDomain = email.ToDomain,
|
||||
ToLocal = email.ToLocal,
|
||||
Date = email.Date,
|
||||
DateSystem = DateTime.SpecifyKind(email.DateSystem, DateTimeKind.Utc),
|
||||
SecondsAgo = (int)DateTime.UtcNow.Subtract(email.DateSystem).TotalSeconds,
|
||||
MessageHtml = email.MessageHtml,
|
||||
MessagePlain = email.MessagePlain,
|
||||
EncryptedSymmetricKey = email.EncryptedSymmetricKey,
|
||||
EncryptionKey = email.EncryptionKey.PublicKey,
|
||||
};
|
||||
|
||||
// Add attachment metadata (without the filebytes)
|
||||
var attachments = context.EmailAttachments.Where(x => x.EmailId == email.Id).Select(x => new AttachmentApiModel()
|
||||
{
|
||||
Id = x.Id,
|
||||
Email_Id = x.EmailId,
|
||||
Filename = x.Filename,
|
||||
MimeType = x.MimeType,
|
||||
Filesize = x.Filesize,
|
||||
}).ToList();
|
||||
|
||||
returnEmail.Attachments = attachments;
|
||||
|
||||
// Enrich HTML by changing all anchor tags to open in new tab
|
||||
if (returnEmail.MessageHtml != null && !string.IsNullOrEmpty(email.MessageHtml))
|
||||
{
|
||||
returnEmail.MessageHtml = ConversionHelper.ConvertAnchorTagsToOpenInNewTab(email.MessageHtml);
|
||||
}
|
||||
|
||||
return Ok(returnEmail);
|
||||
}
|
||||
}
|
||||
58
src/AliasVault.Api/Helpers/ConversionHelper.cs
Normal file
58
src/AliasVault.Api/Helpers/ConversionHelper.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ConversionHelper.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Api.Helpers;
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
using AliasServerDb;
|
||||
|
||||
/// <summary>
|
||||
/// Class which contains various helper methods for data conversion.
|
||||
/// </summary>
|
||||
public class ConversionHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Extract only displayname from full "From" string. E.g. "John Doe" [johndoe@john.com] becomes "John Doe".
|
||||
/// </summary>
|
||||
/// <param name="from">The full from string.</param>
|
||||
/// <returns>Stripped displayname.</returns>
|
||||
public static string ConvertFromToFromDisplay(string from)
|
||||
{
|
||||
// Get the display name from the From field, which is everything before the first < and after the first >
|
||||
string fromDisplay = from;
|
||||
if (from.Contains("<"))
|
||||
{
|
||||
// Remove everything after the last < until the last >
|
||||
fromDisplay = from.Substring(0, from.LastIndexOf("<", StringComparison.Ordinal));
|
||||
|
||||
// Remove any double quotes
|
||||
fromDisplay = fromDisplay.Replace("\"", string.Empty);
|
||||
|
||||
// Trim any whitespace
|
||||
fromDisplay = fromDisplay.Trim();
|
||||
}
|
||||
|
||||
return fromDisplay;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert all anchor tags to open in a new tab.
|
||||
/// </summary>
|
||||
/// <param name="html">HTML input.</param>
|
||||
/// <returns>HTML with all anchor tags converted to open in a new tab when clicked on.</returns>
|
||||
public static string ConvertAnchorTagsToOpenInNewTab(string html)
|
||||
{
|
||||
// Match any <a tag with href attribute, regardless of the position of href or other attributes
|
||||
html = Regex.Replace(
|
||||
html,
|
||||
@"<a\s+(.*?)href=([""'])(.*?)\2(.*?)>",
|
||||
m => $"<a {m.Groups[1].Value}href={m.Groups[2].Value}{m.Groups[3].Value}{m.Groups[2].Value} {m.Groups[4].Value} target=\"_blank\">",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
||||
|
||||
return html;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@using AliasVault.Client.Main.Models.Spamok
|
||||
@using AliasVault.Shared.Models.Spamok
|
||||
|
||||
<div class="fixed inset-0 z-50 overflow-auto bg-gray-500 bg-opacity-75 flex items-center justify-center">
|
||||
<div class="relative p-8 bg-white w-full max-w-md flex-col flex rounded-lg shadow-xl">
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
@using AliasVault.Client.Main.Models.Spamok
|
||||
@using AliasVault.Shared.Models.Spamok
|
||||
@inherits ComponentBase
|
||||
@inject IHttpClientFactory HttpClientFactory
|
||||
@inject HttpClient HttpClient
|
||||
@inject JsInteropService JsInteropService
|
||||
@inject DbService DbService
|
||||
@inject Config Config
|
||||
|
||||
@if (EmailModalVisible)
|
||||
{
|
||||
@@ -21,6 +25,10 @@
|
||||
{
|
||||
<LoadingIndicator/>
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Error))
|
||||
{
|
||||
<AlertMessageError Message="@Error" />
|
||||
}
|
||||
else if (MailboxEmails.Count == 0)
|
||||
{
|
||||
<div>No emails found.</div>
|
||||
@@ -76,6 +84,7 @@
|
||||
private bool ShowComponent { get; set; } = false;
|
||||
private EmailApiModel Email { get; set; } = new();
|
||||
private bool EmailModalVisible { get; set; }
|
||||
private string Error { get; set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -83,12 +92,46 @@
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
// Check if email has a known SpamOK domain, if not, don't show this component.
|
||||
if (EmailAddress.EndsWith("@landmail.nl"))
|
||||
if (IsSpamOkDomain(EmailAddress) || IsAliasVaultDomain(EmailAddress))
|
||||
{
|
||||
ShowComponent = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the email address is from a known SpamOK domain.
|
||||
/// </summary>
|
||||
protected bool IsSpamOkDomain(string email)
|
||||
{
|
||||
return email.EndsWith("@spamok.nl") ||
|
||||
email.EndsWith("@spamok.de") ||
|
||||
email.EndsWith("@spamok.es") ||
|
||||
email.EndsWith("@spamok.fr") ||
|
||||
email.EndsWith("@spamok.com") ||
|
||||
email.EndsWith("@spamok.com.ua") ||
|
||||
email.EndsWith("@landmail.nl") ||
|
||||
email.EndsWith("@landmeel.nl") ||
|
||||
email.EndsWith("@asdasd.nl") ||
|
||||
email.EndsWith("@sdfsdf.nl") ||
|
||||
email.EndsWith("@solarflarecorp.com");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the email address is from a known AliasVault domain.
|
||||
/// </summary>
|
||||
protected bool IsAliasVaultDomain(string email)
|
||||
{
|
||||
foreach (var domain in Config.SmtpAllowedDomains)
|
||||
{
|
||||
if (email.EndsWith(domain))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
@@ -112,19 +155,66 @@
|
||||
return;
|
||||
}
|
||||
|
||||
Error = string.Empty;
|
||||
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
// Get email prefix, which is the part before the @ symbol.
|
||||
string emailPrefix = EmailAddress.Split('@')[0];
|
||||
|
||||
var client = HttpClientFactory.CreateClient("EmailClient");
|
||||
MailboxApiModel? mailbox = await client.GetFromJsonAsync<MailboxApiModel>($"https://api.spamok.com/v2/EmailBox/{emailPrefix}");
|
||||
MailboxApiModel? mailbox = new();
|
||||
|
||||
if (mailbox?.Mails != null)
|
||||
if (IsSpamOkDomain(EmailAddress))
|
||||
{
|
||||
// Show maximum of 10 recent emails.
|
||||
MailboxEmails = mailbox.Mails.Take(10).ToList();
|
||||
// We construct a new HttpClient to avoid using the default one, which is used for the API and sends
|
||||
// the Authorization header. We don't want to send the Authorization header to the external email API.
|
||||
var client = HttpClientFactory.CreateClient("EmailClient");
|
||||
mailbox = await client.GetFromJsonAsync<MailboxApiModel>($"https://api.spamok.com/v2/EmailBox/{emailPrefix}");
|
||||
|
||||
if (mailbox?.Mails != null)
|
||||
{
|
||||
// Show maximum of 10 recent emails.
|
||||
MailboxEmails = mailbox.Mails.Take(10).ToList();
|
||||
}
|
||||
}
|
||||
else if (IsAliasVaultDomain(EmailAddress))
|
||||
{
|
||||
try
|
||||
{
|
||||
mailbox = await HttpClient.GetFromJsonAsync<MailboxApiModel>($"api/v1/EmailBox/{EmailAddress}");
|
||||
if (mailbox?.Mails != null)
|
||||
{
|
||||
// Show maximum of 10 recent emails.
|
||||
MailboxEmails = mailbox.Mails.Take(10).ToList();
|
||||
}
|
||||
|
||||
// Loop through emails and decrypt the subject locally.
|
||||
var context = await DbService.GetDbContextAsync();
|
||||
var privateKeys = await context.EncryptionKeys.ToListAsync();
|
||||
foreach (var mail in MailboxEmails)
|
||||
{
|
||||
var privateKey = privateKeys.FirstOrDefault(x => x.PublicKey == mail.EncryptionKey);
|
||||
if (privateKey is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var decryptedSymmetricKey = await JsInteropService.DecryptWithPrivateKey(mail.EncryptedSymmetricKey, privateKey.PrivateKey);
|
||||
mail.Subject = await JsInteropService.SymmetricDecrypt(mail.Subject, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Error = ex.Message;
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Error = ex.Message;
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
@@ -139,16 +229,59 @@
|
||||
// Get email prefix, which is the part before the @ symbol.
|
||||
string emailPrefix = EmailAddress.Split('@')[0];
|
||||
|
||||
// Load email from API
|
||||
var client = HttpClientFactory.CreateClient("EmailClient");
|
||||
EmailApiModel? mail = await client.GetFromJsonAsync<EmailApiModel>($"https://api.spamok.com/v2/Email/{emailPrefix}/{emailId}");
|
||||
|
||||
if (mail != null)
|
||||
if (IsSpamOkDomain(EmailAddress))
|
||||
{
|
||||
Email = mail;
|
||||
EmailModalVisible = true;
|
||||
StateHasChanged();
|
||||
var client = HttpClientFactory.CreateClient("EmailClient");
|
||||
EmailApiModel? mail = await client.GetFromJsonAsync<EmailApiModel>($"https://api.spamok.com/v2/Email/{emailPrefix}/{emailId}");
|
||||
if (mail != null)
|
||||
{
|
||||
Email = mail;
|
||||
EmailModalVisible = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
else if (IsAliasVaultDomain(EmailAddress))
|
||||
{
|
||||
EmailApiModel? mail = await HttpClient.GetFromJsonAsync<EmailApiModel>($"api/v1/Email/{emailId}");
|
||||
if (mail != null)
|
||||
{
|
||||
// Decrypt the email content locally.
|
||||
var context = await DbService.GetDbContextAsync();
|
||||
var privateKey = await context.EncryptionKeys.FirstOrDefaultAsync(x => x.PublicKey == mail.EncryptionKey);
|
||||
if (privateKey is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var decryptedSymmetricKey = await JsInteropService.DecryptWithPrivateKey(mail.EncryptedSymmetricKey, privateKey.PrivateKey);
|
||||
mail.Subject = await JsInteropService.SymmetricDecrypt(mail.Subject, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
if (mail.MessageHtml is not null)
|
||||
{
|
||||
mail.MessageHtml = await JsInteropService.SymmetricDecrypt(mail.MessageHtml, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
}
|
||||
|
||||
if (mail.MessagePlain is not null)
|
||||
{
|
||||
mail.MessagePlain = await JsInteropService.SymmetricDecrypt(mail.MessagePlain, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
}
|
||||
|
||||
mail.FromDisplay = await JsInteropService.SymmetricDecrypt(mail.FromDisplay, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
mail.FromLocal = await JsInteropService.SymmetricDecrypt(mail.FromLocal, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
mail.FromDomain = await JsInteropService.SymmetricDecrypt(mail.FromDomain, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Error = ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
Email = mail;
|
||||
EmailModalVisible = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@inherits MainBase
|
||||
@using AliasVault.Client.Main.Pages;
|
||||
@inherits AliasVault.Client.Main.Pages.MainBase
|
||||
@implements IDisposable
|
||||
|
||||
<header>
|
||||
|
||||
@@ -11,7 +11,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using AliasClientDb;
|
||||
using AliasVault.Client.Models.FormValidation;
|
||||
using AliasVault.Client.Main.Models.FormValidation;
|
||||
|
||||
/// <summary>
|
||||
/// Credential edit model.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Client.Models.FormValidation;
|
||||
namespace AliasVault.Client.Main.Models.FormValidation;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
|
||||
@@ -14,7 +14,6 @@ using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading.Tasks;
|
||||
using AliasClientDb;
|
||||
using AliasVault.Client.Models;
|
||||
using AliasVault.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Identity = AliasGenerators.Identity.Models.Identity;
|
||||
@@ -279,7 +278,7 @@ public class CredentialService(HttpClient httpClient, DbService dbService)
|
||||
try
|
||||
{
|
||||
var apiReturn =
|
||||
await httpClient.GetFromJsonAsync<FaviconExtractModel>("api/v1/Favicon/Extract?url=" + url);
|
||||
await httpClient.GetFromJsonAsync<FaviconExtractModel>($"api/v1/Favicon/Extract?url={url}");
|
||||
if (apiReturn != null && apiReturn.Image != null)
|
||||
{
|
||||
credentialObject.Service.Logo = apiReturn.Image;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
namespace AliasVault.Client.Services;
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
@@ -93,9 +93,21 @@ public class JsInteropService(IJSRuntime jsRuntime)
|
||||
/// <summary>
|
||||
/// Decrypts a ciphertext with a private key.
|
||||
/// </summary>
|
||||
/// <param name="ciphertext">Ciphertext to decrypt.</param>
|
||||
/// <param name="base64Ciphertext">Ciphertext to decrypt.</param>
|
||||
/// <param name="privateKey">Private key to use for decryption.</param>
|
||||
/// <returns>Decrypted string.</returns>
|
||||
public async Task<string> DecryptWithPrivateKey(string ciphertext, string privateKey) =>
|
||||
await jsRuntime.InvokeAsync<string>("rsaInterop.decryptWithPrivateKey", ciphertext, privateKey);
|
||||
public async Task<byte[]> DecryptWithPrivateKey(string base64Ciphertext, string privateKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Invoke the JavaScript function and get the result as a byte array
|
||||
byte[] result = await jsRuntime.InvokeAsync<byte[]>("rsaInterop.decryptWithPrivateKey", base64Ciphertext, privateKey);
|
||||
return result;
|
||||
}
|
||||
catch (JSException ex)
|
||||
{
|
||||
Console.Error.WriteLine($"JavaScript decryption error: {ex.Message}");
|
||||
throw new CryptographicException("Decryption failed", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,29 +118,42 @@ window.rsaInterop = {
|
||||
* Decrypts a ciphertext string using an RSA private key.
|
||||
* @param {string} ciphertext - The base64-encoded ciphertext to decrypt.
|
||||
* @param {string} privateKey - The private key in JWK format.
|
||||
* @returns {Promise<string>} A promise that resolves to the decrypted plaintext.
|
||||
* @returns {Promise<Uint8Array>} A promise that resolves to the decrypted data as a Uint8Array.
|
||||
*/
|
||||
decryptWithPrivateKey : async function(ciphertext, privateKey) {
|
||||
const privateKeyObj = await window.crypto.subtle.importKey(
|
||||
"jwk",
|
||||
JSON.parse(privateKey),
|
||||
{
|
||||
name: "RSA-OAEP",
|
||||
hash: "SHA-256",
|
||||
},
|
||||
false,
|
||||
["decrypt"]
|
||||
);
|
||||
decryptWithPrivateKey: async function(ciphertext, privateKey) {
|
||||
try {
|
||||
// Parse the private key
|
||||
let parsedPrivateKey = JSON.parse(privateKey);
|
||||
|
||||
const cipherBuffer = Uint8Array.from(atob(ciphertext), c => c.charCodeAt(0));
|
||||
const plaintextBuffer = await window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: "RSA-OAEP"
|
||||
},
|
||||
privateKeyObj,
|
||||
cipherBuffer
|
||||
);
|
||||
// Import the private key
|
||||
let privateKeyObj = await window.crypto.subtle.importKey(
|
||||
"jwk",
|
||||
parsedPrivateKey,
|
||||
{
|
||||
name: "RSA-OAEP",
|
||||
hash: "SHA-256",
|
||||
},
|
||||
true,
|
||||
["decrypt"]
|
||||
);
|
||||
|
||||
return new TextDecoder().decode(plaintextBuffer);
|
||||
// Decode the base64 ciphertext
|
||||
let cipherBuffer = Uint8Array.from(atob(ciphertext), c => c.charCodeAt(0));
|
||||
|
||||
// Decrypt the ciphertext
|
||||
let plaintextBuffer = await window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: "RSA-OAEP",
|
||||
hash: "SHA-256",
|
||||
},
|
||||
privateKeyObj,
|
||||
cipherBuffer
|
||||
);
|
||||
|
||||
// Return the decrypted data as a Uint8Array
|
||||
return new Uint8Array(plaintextBuffer);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to decrypt: ${error.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Client.Main.Models.Spamok;
|
||||
namespace AliasVault.Shared.Models.Spamok;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an attachment for an email.
|
||||
@@ -5,7 +5,7 @@
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Client.Main.Models.Spamok.Base;
|
||||
namespace AliasVault.Shared.Models.Spamok.Base;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mailbox email API model base.
|
||||
@@ -61,4 +61,15 @@ public abstract class EmailApiModelBase
|
||||
/// Gets or sets the number of seconds ago the email was received.
|
||||
/// </summary>
|
||||
public double SecondsAgo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the encrypted symmetric key which was used to encrypt the email message.
|
||||
/// This key is encrypted with the public key of the user.
|
||||
/// </summary>
|
||||
public string EncryptedSymmetricKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the public key of the user used to encrypt the symmetric key.
|
||||
/// </summary>
|
||||
public string EncryptionKey { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -5,9 +5,9 @@
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Client.Main.Models.Spamok;
|
||||
namespace AliasVault.Shared.Models.Spamok;
|
||||
|
||||
using AliasVault.Client.Main.Models.Spamok.Base;
|
||||
using AliasVault.Shared.Models.Spamok.Base;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an email API model.
|
||||
@@ -5,7 +5,7 @@
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Client.Main.Models.Spamok;
|
||||
namespace AliasVault.Shared.Models.Spamok;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mailbox API model.
|
||||
@@ -5,9 +5,9 @@
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Client.Main.Models.Spamok;
|
||||
namespace AliasVault.Shared.Models.Spamok;
|
||||
|
||||
using AliasVault.Client.Main.Models.Spamok.Base;
|
||||
using AliasVault.Shared.Models.Spamok.Base;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mailbox email API model.
|
||||
@@ -123,5 +123,5 @@ public class Email
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of email attachments.
|
||||
/// </summary>
|
||||
public virtual ICollection<EmailAttachment> Attachments { get; set; } = [];
|
||||
public virtual List<EmailAttachment> Attachments { get; set; } = [];
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
using Cryptography;
|
||||
using SmtpServer.Mail;
|
||||
|
||||
namespace AliasVault.SmtpService.Handlers;
|
||||
|
||||
@@ -63,13 +64,15 @@ public class DatabaseMessageStore(ILogger<DatabaseMessageStore> logger, Config c
|
||||
|
||||
stream.Position = 0;
|
||||
var message = await MimeMessage.LoadAsync(stream, cancellationToken);
|
||||
|
||||
// Retrieve all addresses from the SMTP transaction which should contain all recipients for this mail instance.
|
||||
var allAddresses = transaction.To
|
||||
.Distinct()
|
||||
.ToList();
|
||||
// Limit list to 15 addresses max. (to prevent mailbomb spam abuse)
|
||||
|
||||
// Limit list to 15 addresses maximum (to prevent mailbomb spam abuse).
|
||||
var toAddresses = allAddresses.Take(15).ToList();
|
||||
// For every toAddress
|
||||
|
||||
foreach (var toAddress in toAddresses)
|
||||
{
|
||||
// Check if toAddress domain is allowed.
|
||||
@@ -131,7 +134,8 @@ public class DatabaseMessageStore(ILogger<DatabaseMessageStore> logger, Config c
|
||||
return SmtpResponse.NoValidRecipientsGiven;
|
||||
}
|
||||
|
||||
var insertedId = await InsertEmailIntoDatabase(message, userPublicKey);
|
||||
// Set the "to" for the email to the actual one we are looping through now.
|
||||
var insertedId = await InsertEmailIntoDatabase(message, new MailAddress(toAddress.AsAddress()), userPublicKey);
|
||||
logger.LogInformation("Email for {ToAddress} successfully saved into database with ID {insertedId}.",
|
||||
toAddress.User + "@" + toAddress.Host, insertedId);
|
||||
}
|
||||
@@ -149,12 +153,13 @@ public class DatabaseMessageStore(ILogger<DatabaseMessageStore> logger, Config c
|
||||
/// Insert email into database.
|
||||
/// </summary>
|
||||
/// <param name="message">MimeMessage to save into database.</param>
|
||||
/// <param name="toAddress">The recipient for this mail.</param>
|
||||
/// <param name="userEncryptionKey">The public key of the user to encrypt the mail contents with.</param>
|
||||
private async Task<int> InsertEmailIntoDatabase(MimeMessage message, UserEncryptionKey userEncryptionKey)
|
||||
private async Task<int> InsertEmailIntoDatabase(MimeMessage message, MailAddress toAddress, UserEncryptionKey userEncryptionKey)
|
||||
{
|
||||
var dbContext = await dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
var newEmail = ConvertMimeMessageToEmail(message);
|
||||
var newEmail = ConvertMimeMessageToEmail(message, toAddress);
|
||||
newEmail = EmailEncryption.EncryptEmail(newEmail, userEncryptionKey);
|
||||
|
||||
// Insert the email into the database.
|
||||
@@ -168,9 +173,10 @@ public class DatabaseMessageStore(ILogger<DatabaseMessageStore> logger, Config c
|
||||
/// Convert MimeMessage to Email database object.
|
||||
/// </summary>
|
||||
/// <param name="message">MimeMessage object.</param>
|
||||
/// <param name="toAddress">The recipient for this mail.</param>
|
||||
/// <returns>Email object.</returns>
|
||||
/// <exception cref="EmailParseMissingToException"></exception>
|
||||
private static Email ConvertMimeMessageToEmail(MimeMessage message)
|
||||
private static Email ConvertMimeMessageToEmail(MimeMessage message, MailAddress toAddress)
|
||||
{
|
||||
string from = "";
|
||||
|
||||
@@ -195,57 +201,23 @@ public class DatabaseMessageStore(ILogger<DatabaseMessageStore> logger, Config c
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If the above fails, try to find the x-sender in the mail
|
||||
try
|
||||
{
|
||||
MailAddress fromAddress = new MailAddress(message.Headers.First(x => x.Field == "x-sender").Value.ToString());
|
||||
fromLocal = fromAddress.User;
|
||||
fromDomain = fromAddress.Host;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If this fails as well, then simply use a blank value
|
||||
fromLocal = "";
|
||||
fromDomain = "";
|
||||
}
|
||||
}
|
||||
|
||||
MailAddress toAddress;
|
||||
string to;
|
||||
|
||||
// Try to extract to address firstly from x-receiver address..
|
||||
try
|
||||
{
|
||||
to = message.Headers.First(x => x.Field == "x-receiver").Value.ToString();
|
||||
toAddress = new MailAddress(to);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If the above fails, try to find the "to" in the mail
|
||||
try
|
||||
{
|
||||
to = message.To.FirstOrDefault()?.ToString() ?? "";
|
||||
toAddress = new MailAddress(to);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If this fails as well, then simply let it throw an error to the caller.
|
||||
throw new EmailParseMissingToException("Could not find x-receiver or to address in email.");
|
||||
}
|
||||
// If this fails, then simply use a blank value
|
||||
fromLocal = "";
|
||||
fromDomain = "";
|
||||
}
|
||||
|
||||
// Create email object
|
||||
var email = new Email();
|
||||
email.From = from;
|
||||
email.FromLocal = fromLocal;
|
||||
email.FromDomain = fromDomain;
|
||||
email.FromLocal = fromLocal.ToLower();
|
||||
email.FromDomain = fromDomain.ToLower();
|
||||
|
||||
email.To = to;
|
||||
// Local part to lowercase, as mailboxes are always lowercase
|
||||
email.To = toAddress.Address.ToLower();
|
||||
email.ToLocal = toAddress.User.ToLower();
|
||||
email.ToDomain = toAddress.Host;
|
||||
email.ToDomain = toAddress.Host.ToLower();
|
||||
|
||||
email.Subject = message.Subject ?? "";
|
||||
email.Subject = message.Subject ?? string.Empty;
|
||||
email.MessageHtml = message.HtmlBody;
|
||||
email.MessagePlain = message.TextBody;
|
||||
email.MessageSource = message.ToString();
|
||||
|
||||
@@ -1 +1 @@
|
||||
curl --url "smtp://localhost:25" --mail-from "sender@example.com" --mail-rcpt "yourname@example.tld" --upload-file testEmail1.txt
|
||||
curl --url "smtp://localhost:25" --mail-from "sender@example.com" --mail-rcpt "test@example.tld" --upload-file testEmail1.txt
|
||||
|
||||
@@ -124,7 +124,7 @@ public class SmtpServerTests
|
||||
var processedEmail = await _testHostBuilder.GetDbContext().Emails.FirstAsync();
|
||||
|
||||
// Test non-encrypted field.
|
||||
Assert.That(processedEmail.To, Is.EqualTo("\"Test Recipient\" <claimed@example.tld>"));
|
||||
Assert.That(processedEmail.To, Is.EqualTo("claimed@example.tld"));
|
||||
|
||||
// Decrypt the email and then check all individual fields.
|
||||
processedEmail = EmailEncryption.DecryptEmail(processedEmail, PrivateKey);
|
||||
@@ -159,7 +159,7 @@ public class SmtpServerTests
|
||||
var processedEmail = await _testHostBuilder.GetDbContext().Emails.FirstAsync();
|
||||
|
||||
// Test non-encrypted field.
|
||||
Assert.That(processedEmail.To, Is.EqualTo("\"Test Recipient\" <claimed@example.tld>"));
|
||||
Assert.That(processedEmail.To, Is.EqualTo("claimed@example.tld"));
|
||||
|
||||
// Decrypt the email and then check all individual fields.
|
||||
processedEmail = EmailEncryption.DecryptEmail(processedEmail, PrivateKey);
|
||||
@@ -192,7 +192,7 @@ public class SmtpServerTests
|
||||
var processedEmail = await _testHostBuilder.GetDbContext().Emails.FirstAsync();
|
||||
|
||||
// Test non-encrypted field.
|
||||
Assert.That(processedEmail.To, Is.EqualTo("\"Test Recipient\" <claimed@example.tld>"));
|
||||
Assert.That(processedEmail.To, Is.EqualTo("claimed@example.tld"));
|
||||
|
||||
// Decrypt the email and then check all individual fields.
|
||||
processedEmail = EmailEncryption.DecryptEmail(processedEmail, PrivateKey);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ConversionHelperTest.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Tests.Helpers;
|
||||
|
||||
using AliasVault.Api.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the CsvImportExport class.
|
||||
/// </summary>
|
||||
public class ConversionHelperTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests the conversion of an email address with a display name to just the display name.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestFromConversion()
|
||||
{
|
||||
string from = "\"My full Name\" <myname@example.com>";
|
||||
string convertedFrom = ConversionHelper.ConvertFromToFromDisplay(from);
|
||||
|
||||
// Check that conversion works as expected.
|
||||
Assert.That(convertedFrom, Is.EqualTo("My full Name"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the conversion of a simple anchor tag to open in a new tab.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestAnchorTabConversionSimple()
|
||||
{
|
||||
string anchorHtml = "<a href=\"https://dutchamzmasters.lt.acemlnb.com/Prod/link-tracker?redirectUrl=aHR0cHMlM0ElMkYlMkZ3d3cuZHV0Y2hhbXptYXN0ZXJzLmNvbSUyRnRoYW5rLXlvdTloN3poZ3Rp&sig=CpED3rRPX48ddoWTUZURadAYPYgPppT312jUNnvUCPo5&iat=1679512450&a=%7C%7C25799960%7C%7C&account=dutchamzmasters%2Eactivehosted%2Ecom&email=DQeVbqE%2Fy2FD5V3I2cvSxXjJCI3Tg5qfUHKGneOhzjJYZ1kM3LVZcQ%3D%3D%3AvdAW7N7fs1pZlI1ib%2BNbsMYz5m4FssAR&s=5241db963ffe25d6f4b762fc00038ee2&i=163A299A10A816\"></a>";
|
||||
string convertedAnchorTags = ConversionHelper.ConvertAnchorTagsToOpenInNewTab(anchorHtml);
|
||||
|
||||
// Check that conversion works as expected.
|
||||
Assert.That(convertedAnchorTags, Does.Contain("target=\"_blank\""));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the conversion of a complex anchor tag with multiple attributes to open in a new tab.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestAnchorTabConversionComplex1()
|
||||
{
|
||||
string anchorHtml = "<a\nhref=\"https://dutchamzmasters.lt.acemlnb.com/Prod/link-tracker?redirectUrl=aHR0cHMlM0ElMkYlMkZ3d3cuZHV0Y2hhbXptYXN0ZXJzLmNvbSUyRnRoYW5rLXlvdTloN3poZ3Rp&sig=CpED3rRPX48ddoWTUZURadAYPYgPppT312jUNnvUCPo5&iat=1679512450&a=%7C%7C25799960%7C%7C&account=dutchamzmasters%2Eactivehosted%2Ecom&email=DQeVbqE%2Fy2FD5V3I2cvSxXjJCI3Tg5qfUHKGneOhzjJYZ1kM3LVZcQ%3D%3D%3AvdAW7N7fs1pZlI1ib%2BNbsMYz5m4FssAR&s=5241db963ffe25d6f4b762fc00038ee2&i=163A299A10A816\" data-ac-default-color=\"1\" style=\"margin: 0; outline: none; padding: 0; color: #045FB4; text-decoration: underline; font-weight: bold;\"><span style=\"color: ; font-size: inherit; font-weight: inherit; line-height: inherit; text-decoration: inherit;\">Start hier met de training >>></span></a>";
|
||||
string convertedAnchorTags = ConversionHelper.ConvertAnchorTagsToOpenInNewTab(anchorHtml);
|
||||
|
||||
// Check that conversion works as expected.
|
||||
Assert.That(convertedAnchorTags, Does.Contain("target=\"_blank\""));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the conversion of a complex anchor tag with nested elements to open in a new tab.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestAnchorTabConversionComplex2()
|
||||
{
|
||||
string anchorHtml = "<div class=\"btn btn--flat btn--large\" style=\"Margin-bottom: 20px;text-align: center;\">\n <!--[if !mso]><!--><a style=\"border-radius: 4px;display: inline-block;font-size: 14px;font-weight: bold;line-height: 24px;padding: 12px 24px;text-align: center;text-decoration: none !important;transition: opacity 0.1s ease-in;color: #212529 !important;background-color: #ffdd55;font-family: Open Sans, sans-serif;\" href=\"https://eazegamesbv.cmail19.com/t/j-l-sktidll-ddhdthjhkj-j/\">Haal je beloning op</a><!--<![endif]-->\n <!--[if mso]><p style=\"line-height:0;margin:0;\"> </p><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" href=\"https://eazegamesbv.cmail19.com/t/j-l-sktidll-ddhdthjhkj-j/\" style=\"width:136.5pt\" arcsize=\"9%\" fillcolor=\"#FFDD55\" stroke=\"f\"><v:textbox style=\"mso-fit-shape-to-text:t\" inset=\"0pt,8.25pt,0pt,8.25pt\"><center style=\"font-size:14px;line-height:24px;color:#212529;font-family:Open Sans,sans-serif;font-weight:bold;mso-line-height-rule:exactly;mso-text-raise:1.5px\">Haal je beloning op</center></v:textbox></v:roundrect><![endif]--></div>";
|
||||
string convertedAnchorTags = ConversionHelper.ConvertAnchorTagsToOpenInNewTab(anchorHtml);
|
||||
|
||||
// Check that conversion works as expected.
|
||||
Assert.That(convertedAnchorTags, Does.Contain("target=\"_blank\""));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the conversion of a complex anchor tag within a table cell to open in a new tab.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestAnchorTabConversionComplex3()
|
||||
{
|
||||
string anchorHtml = "<td style=\"word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; font-family: Helvetica, Arial, sans-serif; font-weight: normal; margin: 0; Margin: 0; font-size: 16px; line-height: 1.3; text-align: center; color: #fefefe; background: #f17130; border-radius: 5px; border: 0 solid #f17130; width: 400px; padding: 5px; border-collapse: collapse;\"><a href=\"https://click.info.wijkopenautos.nl/f/a/chSbfTJZeP5dGZaVjOIUlw~~/AABMyAA~/RgRnzW26P0SwaHR0cHM6Ly93d3cud2lqa29wZW5hdXRvcy5ubC9pbnNwZWN0aW9uL2UwZTg0M2Y4NGEzZDRjYjc4MDYwMGU2NDEzNzc1NmEyLz9NSUQ9TkxfQ1JNXzFfM18wXzE5NjY3MF8yNDE5NTgwMTEyNDdfMSZ0bXM9MTcwOTg5Mzc4MyZ1dG1fc291cmNlPUNSTSZ1dG1fbWVkaXVtPWVtYWlsJnV0bV9jYW1wYWlnbj0zXzdXBXNwY2V1Qgpl6bro6mX9yH0mUhJidWxhYmVlckBhc2Rhc2QubmxYBAAAAAw~\" style=\"margin: 0; Margin: 0; line-height: 1.3; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: bold; color: #fefefe; text-decoration: none; display: inline-block; background: #f17130; border: 0 solid #f17130; width: 400px; text-align: center; padding: 5px; border-radius: 5px;\">Ontvang nu jouw prijs <b>></b></a></td>";
|
||||
string convertedAnchorTags = ConversionHelper.ConvertAnchorTagsToOpenInNewTab(anchorHtml);
|
||||
|
||||
// Check that conversion works as expected.
|
||||
Assert.That(convertedAnchorTags, Does.Contain("target=\"_blank\""));
|
||||
}
|
||||
}
|
||||
@@ -38,10 +38,13 @@ public static class Encryption
|
||||
/// <returns>The encrypted symmetric key as a base64-encoded string.</returns>
|
||||
public static string EncryptSymmetricKeyWithRsa(byte[] symmetricKey, string publicKey)
|
||||
{
|
||||
using (var rsa = new RSACryptoServiceProvider())
|
||||
using (var rsa = RSA.Create())
|
||||
{
|
||||
ImportPublicKey(rsa, publicKey);
|
||||
byte[] encryptedKey = rsa.Encrypt(symmetricKey, true);
|
||||
rsa.KeySize = 2048;
|
||||
var rsaParams = RSAEncryptionPadding.OaepSHA256;
|
||||
|
||||
byte[] encryptedKey = rsa.Encrypt(symmetricKey, rsaParams);
|
||||
return Convert.ToBase64String(encryptedKey);
|
||||
}
|
||||
}
|
||||
@@ -54,11 +57,14 @@ public static class Encryption
|
||||
/// <returns>The encrypted symmetric key as a base64-encoded string.</returns>
|
||||
public static byte[] DecryptSymmetricKeyWithRsa(string ciphertext, string privateKey)
|
||||
{
|
||||
using (var rsa = new RSACryptoServiceProvider())
|
||||
using (var rsa = RSA.Create())
|
||||
{
|
||||
ImportPrivateKey(rsa, privateKey);
|
||||
rsa.KeySize = 2048;
|
||||
var rsaParams = RSAEncryptionPadding.OaepSHA256;
|
||||
|
||||
byte[] cipherBytes = Convert.FromBase64String(ciphertext);
|
||||
return rsa.Decrypt(cipherBytes, true);
|
||||
return rsa.Decrypt(cipherBytes, rsaParams);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +184,7 @@ public static class Encryption
|
||||
/// </summary>
|
||||
/// <param name="rsa">The RSA provider to import the key into.</param>
|
||||
/// <param name="jwk">The public key in JWK format.</param>
|
||||
private static void ImportPublicKey(RSACryptoServiceProvider rsa, string jwk)
|
||||
private static void ImportPublicKey(RSA rsa, string jwk)
|
||||
{
|
||||
var jwkObj = JsonSerializer.Deserialize<JsonElement>(jwk);
|
||||
var n = Base64UrlDecode(jwkObj.GetProperty("n").GetString()!);
|
||||
@@ -198,7 +204,7 @@ public static class Encryption
|
||||
/// </summary>
|
||||
/// <param name="rsa">The RSA provider to import the key into.</param>
|
||||
/// <param name="jwk">The private key in JWK format.</param>
|
||||
private static void ImportPrivateKey(RSACryptoServiceProvider rsa, string jwk)
|
||||
private static void ImportPrivateKey(RSA rsa, string jwk)
|
||||
{
|
||||
var jwkObj = JsonSerializer.Deserialize<JsonElement>(jwk);
|
||||
var n = Base64UrlDecode(jwkObj.GetProperty("n").GetString()!);
|
||||
|
||||
Reference in New Issue
Block a user