Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;

namespace ByteSync.Interfaces.Controls.Encryptions;

public interface IMergerDecrypter
public interface IMergerDecrypter : IDisposable
{
Task MergeAndDecrypt();
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using ByteSync.Interfaces.Controls.Encryptions;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Security.Cryptography;
using ByteSync.Business.Communications.Downloading;
using ByteSync.Interfaces.Controls.Communications;

Expand Down Expand Up @@ -31,15 +34,34 @@ public async Task MergeAsync(int partToMerge)
{
foreach (var mergerDecrypter in _mergerDecrypters)
{
await mergerDecrypter.MergeAndDecrypt();
try
{
await mergerDecrypter.MergeAndDecrypt();
}
finally
{
mergerDecrypter.Dispose();
}
}

_downloadTarget.RemoveMemoryStream(partToMerge);
}
catch
catch (InvalidOperationException ex)
{
// Log security-related exceptions without exposing sensitive details
await _errorManager.SetOnErrorAsync();
throw;
throw new InvalidOperationException("Encryption operation failed", ex);
}
catch (CryptographicException ex)
{
// Handle cryptographic failures securely
await _errorManager.SetOnErrorAsync();
throw new InvalidOperationException("Cryptographic operation failed", ex);
}
catch (Exception ex)
{
await _errorManager.SetOnErrorAsync();
throw new InvalidOperationException("Merge operation failed", ex);
}
}
finally
Expand Down
33 changes: 28 additions & 5 deletions src/ByteSync.Client/Services/Encryptions/MergerDecrypter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
using ByteSync.Common.Business.SharedFiles;
using ByteSync.Interfaces.Controls.Encryptions;
using ByteSync.Interfaces.Repositories;
using Microsoft.Extensions.Logging;

namespace ByteSync.Services.Encryptions;

public class MergerDecrypter : IMergerDecrypter
public class MergerDecrypter : IMergerDecrypter, IDisposable
{
private readonly ICloudSessionConnectionRepository _cloudSessionConnectionRepository;
private readonly ILogger<MergerDecrypter> _logger;
Expand All @@ -31,11 +32,11 @@ public MergerDecrypter(string localPath, DownloadTarget downloadTarget, Cancella

public SharedFileDefinition SharedFileDefinition { get; private set; } = null!;

private Aes Aes { get; set; } = null!;
private Aes? Aes { get; set; }

private DownloadTarget DownloadTarget { get; set; } = null!;

private CancellationTokenSource CancellationTokenSource { get; set; }
private CancellationTokenSource CancellationTokenSource { get; set; } = null!;

private void Initialize(string finalFile, DownloadTarget downloadTarget, CancellationTokenSource cancellationTokenSource)
{
Expand All @@ -45,7 +46,19 @@ private void Initialize(string finalFile, DownloadTarget downloadTarget, Cancell
SharedFileDefinition = downloadTarget.SharedFileDefinition;

Aes = Aes.Create();
Aes.Key = _cloudSessionConnectionRepository.GetAesEncryptionKey()!;

var encryptionKey = _cloudSessionConnectionRepository.GetAesEncryptionKey();
if (encryptionKey == null)
{
throw new InvalidOperationException("Encryption key is not available");
}

if (SharedFileDefinition.IV == null || SharedFileDefinition.IV.Length == 0)
{
throw new InvalidOperationException("Invalid IV provided");
}

Aes.Key = encryptionKey;
Aes.IV = SharedFileDefinition.IV;

CancellationTokenSource = cancellationTokenSource;
Expand All @@ -67,10 +80,15 @@ public async Task MergeAndDecrypt()
{
return;
}

if (Aes == null)
{
throw new InvalidOperationException("AES encryption not initialized");
}

await using var outStream = new FileStream(FinalFile, FileMode.Append);

var cryptoTransform = Aes.CreateDecryptor(Aes.Key, Aes.IV);
using var cryptoTransform = Aes.CreateDecryptor(Aes.Key, Aes.IV);
await using var cryptoStream = new CryptoStream(outStream, cryptoTransform, CryptoStreamMode.Write);

TotalReadFiles += 1;
Expand All @@ -81,4 +99,9 @@ public async Task MergeAndDecrypt()
memoryStream.Position = 0;
await memoryStream.CopyToAsync(cryptoStream, CancellationTokenSource.Token);
}

public void Dispose()
{
Aes?.Dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ public void TryRevertOnError(Exception exception)
{
try
{
_logger.LogWarning("An exception has occurred during validation operations on file {file}: {type} - {message}. Trying to revert operation",
DestinationFullName, exception.GetType().Name, exception.Message);
_logger.LogWarning("An exception has occurred during validation operations on file {file}: {type}. Trying to revert operation",
DestinationFullName, exception.GetType().Name);
if (!HasValidationStarted)
{
// We try to delete the DestinationTemporaryPath file if it exists
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using ByteSync.Interfaces.Repositories;
using ByteSync.Interfaces.Services.Sessions;
using ByteSync.Interfaces.Services.Sessions.Connecting;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Unit = System.Reactive.Unit;
Expand Down Expand Up @@ -122,7 +123,8 @@ private void UpdateErrorMessage(string? errorMessageSource, Exception? exception
string errorMessage = _localizationService[ErrorMessageSource!];
if (exception != null)
{
errorMessage += $" ({exception.Message})";
_logger.LogError(exception, "Session creation error occurred");
// Don't expose exception details to the user for security reasons
}

ErrorMessage = errorMessage;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using ByteSync.Interfaces.Repositories;
using ByteSync.Interfaces.Services.Sessions.Connecting;
using ByteSync.Interfaces.Services.Sessions.Connecting.Joining;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;

Expand Down Expand Up @@ -143,7 +144,8 @@ private void UpdateErrorMessage(string? errorMessageSource, Exception? exception
string errorMessage = _localizationService[ErrorMessageSource!];
if (exception != null)
{
errorMessage += $" ({exception.Message})";
_logger.LogError(exception, "Session join error occurred");
// Don't expose exception details to the user for security reasons
}

ErrorMessage = errorMessage;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using ByteSync.Interfaces.Controls.Encryptions;
using ByteSync.Services.Communications.Transfers;
using FluentAssertions;
using System.Security.Cryptography;

namespace ByteSync.Tests.Services.Communications.Transfers;

Expand All @@ -25,6 +26,8 @@ public async Task MergeAsync_CallsAllDecryptersAndRemovesMemoryStreamAndNotifies
await fileMerger.MergeAsync(42);
decrypter1.Verify(d => d.MergeAndDecrypt(), Times.Once);
decrypter2.Verify(d => d.MergeAndDecrypt(), Times.Once);
decrypter1.Verify(d => d.Dispose(), Times.Once);
decrypter2.Verify(d => d.Dispose(), Times.Once);
}

[Test]
Expand All @@ -41,8 +44,29 @@ public async Task MergeAsync_OnError_CallsOnErrorAndThrows()
downloadTarget,
new SemaphoreSlim(1, 1)
);
await FluentActions.Invoking(async () => await fileMerger.MergeAsync(1)).Should().ThrowAsync<Exception>();
await FluentActions.Invoking(async () => await fileMerger.MergeAsync(1)).Should().ThrowAsync<InvalidOperationException>();
errorManager.Verify(e => e.SetOnErrorAsync(), Times.Once);
decrypter.Verify(d => d.Dispose(), Times.Once);
}

[Test]
public async Task MergeAsync_OnCryptographicError_CallsOnErrorAndThrowsSecureException()
{
var decrypter = new Mock<IMergerDecrypter>();
decrypter.Setup(d => d.MergeAndDecrypt()).ThrowsAsync(new CryptographicException("crypto fail"));
var errorManager = new Mock<IErrorManager>();
errorManager.Setup(e => e.SetOnErrorAsync()).Returns(Task.CompletedTask).Verifiable();
var downloadTarget = new ByteSync.Business.Communications.Downloading.DownloadTarget(null!, null, new HashSet<string>());
var fileMerger = new FileMerger(
new List<IMergerDecrypter> { decrypter.Object },
errorManager.Object,
downloadTarget,
new SemaphoreSlim(1, 1)
);
var exception = await FluentActions.Invoking(async () => await fileMerger.MergeAsync(1)).Should().ThrowAsync<InvalidOperationException>();
exception.WithMessage("Cryptographic operation failed");
errorManager.Verify(e => e.SetOnErrorAsync(), Times.Once);
decrypter.Verify(d => d.Dispose(), Times.Once);
}

}