✨ File encryption
✨ Shared login status across sites
This commit is contained in:
@@ -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; }
|
||||
|
60
DysonNetwork.Drive/Storage/FileEncryptor.cs
Normal file
60
DysonNetwork.Drive/Storage/FileEncryptor.cs
Normal 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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
@@ -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);
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user