File encryption

 Shared login status across sites
This commit is contained in:
2025-07-26 01:37:23 +08:00
parent 081f3f609e
commit 0486c0d0e5
23 changed files with 670 additions and 65 deletions

View File

@@ -42,6 +42,7 @@ public class CloudFile : ModelBase, ICloudFile, IIdentifiedResource
public Instant? UploadedAt { get; set; }
public bool HasCompression { get; set; } = false;
public bool HasThumbnail { get; set; } = false;
public bool IsEncrypted { get; set; } = false;
[JsonIgnore] public FilePool? Pool { get; set; }
public Guid? PoolId { get; set; }

View File

@@ -0,0 +1,60 @@
using System.Security.Cryptography;
namespace DysonNetwork.Drive.Storage;
public static class FileEncryptor
{
public static void EncryptFile(string inputPath, string outputPath, string password)
{
var salt = RandomNumberGenerator.GetBytes(16);
var key = DeriveKey(password, salt, 32);
var nonce = RandomNumberGenerator.GetBytes(12); // For AES-GCM
using var aes = new AesGcm(key, 16); // Specify 16-byte tag size explicitly
var plaintext = File.ReadAllBytes(inputPath);
var magic = "DYSON1"u8.ToArray();
var contentWithMagic = new byte[magic.Length + plaintext.Length];
Buffer.BlockCopy(magic, 0, contentWithMagic, 0, magic.Length);
Buffer.BlockCopy(plaintext, 0, contentWithMagic, magic.Length, plaintext.Length);
var ciphertext = new byte[contentWithMagic.Length];
var tag = new byte[16];
aes.Encrypt(nonce, contentWithMagic, ciphertext, tag);
// Save as: [salt (16)][nonce (12)][tag (16)][ciphertext]
using var fs = new FileStream(outputPath, FileMode.Create, FileAccess.Write);
fs.Write(salt);
fs.Write(nonce);
fs.Write(tag);
fs.Write(ciphertext);
}
public static void DecryptFile(string inputPath, string outputPath, string password)
{
var input = File.ReadAllBytes(inputPath);
var salt = input[..16];
var nonce = input[16..28];
var tag = input[28..44];
var ciphertext = input[44..];
var key = DeriveKey(password, salt, 32);
var decrypted = new byte[ciphertext.Length];
using var aes = new AesGcm(key, 16); // Specify 16-byte tag size explicitly
aes.Decrypt(nonce, ciphertext, tag, decrypted);
var magic = "DYSON1"u8.ToArray();
if (magic.Where((t, i) => decrypted[i] != t).Any())
throw new CryptographicException("Incorrect password or corrupted file.");
var plaintext = decrypted[magic.Length..];
File.WriteAllBytes(outputPath, plaintext);
}
private static byte[] DeriveKey(string password, byte[] salt, int keyBytes)
{
using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100_000, HashAlgorithmName.SHA256);
return pbkdf2.GetBytes(keyBytes);
}
}

View File

@@ -7,8 +7,6 @@ namespace DysonNetwork.Drive.Storage;
public class RemoteStorageConfig
{
public string Id { get; set; } = string.Empty;
public string Label { get; set; } = string.Empty;
public string Region { get; set; } = string.Empty;
public string Bucket { get; set; } = string.Empty;
public string Endpoint { get; set; } = string.Empty;

View File

@@ -104,14 +104,24 @@ public class FileService(
string fileId,
Stream stream,
string fileName,
string? contentType
string? contentType,
string? encryptPassword
)
{
var ogFilePath = Path.GetFullPath(Path.Join(configuration.GetValue<string>("Tus:StorePath"), fileId));
var fileSize = stream.Length;
var hash = await HashFileAsync(stream, fileSize: fileSize);
contentType ??= !fileName.Contains('.') ? "application/octet-stream" : MimeTypes.GetMimeType(fileName);
if (!string.IsNullOrWhiteSpace(encryptPassword))
{
var encryptedPath = Path.Combine(Path.GetTempPath(), $"{fileId}.encrypted");
FileEncryptor.EncryptFile(ogFilePath, encryptedPath, encryptPassword);
File.Delete(ogFilePath); // Delete original unencrypted
File.Move(encryptedPath, ogFilePath); // Replace the original one with encrypted
}
var hash = await HashFileAsync(stream, fileSize: fileSize);
var file = new CloudFile
{
Id = fileId,
@@ -119,7 +129,8 @@ public class FileService(
MimeType = contentType,
Size = fileSize,
Hash = hash,
AccountId = Guid.Parse(account.Id)
AccountId = Guid.Parse(account.Id),
IsEncrypted = !string.IsNullOrWhiteSpace(encryptPassword)
};
var existingFile = await db.Files.AsNoTracking().FirstOrDefaultAsync(f => f.Hash == hash);

View File

@@ -62,8 +62,17 @@ public abstract class TusService
var fileStream = await file.GetContentAsync(eventContext.CancellationToken);
var encryptPassword = httpContext.Request.Headers["X-FilePass"].FirstOrDefault();
var fileService = services.GetRequiredService<FileService>();
var info = await fileService.ProcessNewFileAsync(user, file.Id, fileStream, fileName, contentType);
var info = await fileService.ProcessNewFileAsync(
user,
file.Id,
fileStream,
fileName,
contentType,
encryptPassword
);
using var finalScope = eventContext.HttpContext.RequestServices.CreateScope();
var jsonOptions = finalScope.ServiceProvider.GetRequiredService<IOptions<JsonOptions>>().Value