From 6492cd79038413cba2693b8456f948b7a126068d Mon Sep 17 00:00:00 2001 From: Robin Ehlert Date: Mon, 21 Apr 2025 20:47:08 +0200 Subject: [PATCH] initial commit --- .gitignore | 4 + Cryptomator.sln | 16 ++++ Cryptomator/Commands/CreateKeyPair.cs | 24 ++++++ Cryptomator/Commands/Decrypt.cs | 77 +++++++++++++++++++ .../Commands/Encrypt/EncryptCommand.cs | 42 ++++++++++ Cryptomator/Commands/Encrypt/Step1Config.cs | 46 +++++++++++ Cryptomator/Commands/Encrypt/Step2Rsa.cs | 42 ++++++++++ Cryptomator/Commands/Encrypt/Step3Encrypt.cs | 51 ++++++++++++ Cryptomator/Cryptomator.csproj | 15 ++++ Cryptomator/Program.cs | 21 +++++ Cryptomator/RoP.cs | 58 ++++++++++++++ 11 files changed, 396 insertions(+) create mode 100644 .gitignore create mode 100644 Cryptomator.sln create mode 100644 Cryptomator/Commands/CreateKeyPair.cs create mode 100644 Cryptomator/Commands/Decrypt.cs create mode 100644 Cryptomator/Commands/Encrypt/EncryptCommand.cs create mode 100644 Cryptomator/Commands/Encrypt/Step1Config.cs create mode 100644 Cryptomator/Commands/Encrypt/Step2Rsa.cs create mode 100644 Cryptomator/Commands/Encrypt/Step3Encrypt.cs create mode 100644 Cryptomator/Cryptomator.csproj create mode 100644 Cryptomator/Program.cs create mode 100644 Cryptomator/RoP.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e12c11b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +bin/ +obj/ +.idea/ +*.user diff --git a/Cryptomator.sln b/Cryptomator.sln new file mode 100644 index 0000000..ee90fcf --- /dev/null +++ b/Cryptomator.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cryptomator", "Cryptomator\Cryptomator.csproj", "{C8109960-0F0D-40D7-861D-1C809E160111}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C8109960-0F0D-40D7-861D-1C809E160111}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C8109960-0F0D-40D7-861D-1C809E160111}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8109960-0F0D-40D7-861D-1C809E160111}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C8109960-0F0D-40D7-861D-1C809E160111}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Cryptomator/Commands/CreateKeyPair.cs b/Cryptomator/Commands/CreateKeyPair.cs new file mode 100644 index 0000000..194e2a2 --- /dev/null +++ b/Cryptomator/Commands/CreateKeyPair.cs @@ -0,0 +1,24 @@ +using System.Security.Cryptography; +using Spectre.Console.Cli; + +namespace Cryptomator.Commands; + +public sealed class CreateKeyPair: AsyncCommand +{ + public sealed class Settings : CommandSettings + { + [CommandArgument(0, "<./>")] + public string Path { get; set; } + } + + public override async Task ExecuteAsync(CommandContext context, Settings settings) + { + using var rsa = RSA.Create(); + var privateKey = rsa.ExportRSAPrivateKeyPem(); + var t1 = File.WriteAllTextAsync($"{settings.Path}/priv.pem", privateKey); + var pem = rsa.ExportRSAPublicKeyPem(); + var t2 = File.WriteAllTextAsync($"{settings.Path}/pub.pem", pem); + await Task.WhenAll(t1, t2); + return 0; + } +} \ No newline at end of file diff --git a/Cryptomator/Commands/Decrypt.cs b/Cryptomator/Commands/Decrypt.cs new file mode 100644 index 0000000..e7e247d --- /dev/null +++ b/Cryptomator/Commands/Decrypt.cs @@ -0,0 +1,77 @@ +using System.Security.Cryptography; +using Spectre.Console.Cli; + +namespace Cryptomator.Commands; + +public sealed class Decrypt: AsyncCommand +{ + public sealed class Settings : CommandSettings + { + [CommandArgument(0, "<./>")] + [CommandOption("-k|--key")] + public string PrivateKeyPath { get; set; } + [CommandArgument(1, "<./>")] + [CommandOption("-i|--input")] + public string EncryptedDirectory { get; set; } + [CommandArgument(2, "<./>")] + [CommandOption("-o|--output")] + public string OutputDirectory { get; set; } + } + + public override async Task ExecuteAsync(CommandContext context, Settings settings) + { + using var rsa = await LoadPrivateKey(settings.PrivateKeyPath); + var files = GetEncryptedFiles(settings.EncryptedDirectory); + await Parallel.ForEachAsync(files, async (file, ct) => + { + await DecryptFile(file, settings.EncryptedDirectory, settings.OutputDirectory, rsa); + }); + return 0; + } + + private static async Task LoadPrivateKey(string privateKeyPath) + { + var content = await File.ReadAllTextAsync(privateKeyPath); + var rsa = RSA.Create(); + rsa.ImportFromPem(content); + return rsa; + } + + private string[] GetEncryptedFiles(string encryptedDirectory) + { + var files = Directory.GetFiles(encryptedDirectory) + .GroupBy(Path.GetFileNameWithoutExtension) + .Select(g => g.First()) + .ToArray(); + return files; + } + + private async Task DecryptFile(string encryptedFile, string inputDirectory, string outputDirectory, RSA rsa) + { + var name = Path.GetFileNameWithoutExtension(encryptedFile); + if (!File.Exists($"{inputDirectory}/{name}.key")) + { + return; + } + var key = File.ReadAllBytesAsync($"{inputDirectory}/{name}.key"); + var iv = File.ReadAllBytesAsync($"{inputDirectory}/{name}.iv"); + var keyBytes = await key; + var decryptedKey = rsa.Decrypt(keyBytes, RSAEncryptionPadding.OaepSHA512); + var ivBytes = await iv; + var decryptedIv = rsa.Decrypt(ivBytes, RSAEncryptionPadding.OaepSHA512); + + var aes = Aes.Create(); + aes.KeySize = 256; + aes.Key = decryptedKey; + aes.IV = decryptedIv; + + await using var inFs = File.OpenRead($"{inputDirectory}/{name}.enc"); + await using var outFs = File.OpenWrite($"{outputDirectory}/{name}"); + await using var encStream = new CryptoStream( + inFs, + aes.CreateDecryptor(), + CryptoStreamMode.Read + ); + await encStream.CopyToAsync(outFs); + } +} \ No newline at end of file diff --git a/Cryptomator/Commands/Encrypt/EncryptCommand.cs b/Cryptomator/Commands/Encrypt/EncryptCommand.cs new file mode 100644 index 0000000..b1dc08f --- /dev/null +++ b/Cryptomator/Commands/Encrypt/EncryptCommand.cs @@ -0,0 +1,42 @@ +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace Cryptomator.Commands.Encrypt; + +public sealed class EncryptCommand : AsyncCommand +{ + public sealed class Settings : CommandSettings + { + [CommandOption("-p|--pubkey")] + [CommandArgument(0, "<./pubkey.asc>")] + [Description("Path to the public key to encrypt with")] + public string PublikKeyPath { get; set; } + + [CommandOption("-i|--input")] + [CommandArgument(1, "<./input-dir>")] + [Description("Path to the directory to encrypt")] + public string InputDir { get; set; } + + [CommandOption("-o|--output")] + [CommandArgument(2, "<./output-dir>")] + [Description("Path to the directory to write the encrypted files to")] + public string OutputDir { get; set; } + } + + public override async Task ExecuteAsync(CommandContext context, Settings settings) + { + var config = await Step1Config.FromSettings(settings) + .ContinueWith(Step2Rsa.FromConfig) + .ContinueWith(Step3Encrypt.FromStep2); + var result = config.Match( + _ => 0, + err => + { + Console.WriteLine(err); + return 1; + } + ); + + return result; + } +} \ No newline at end of file diff --git a/Cryptomator/Commands/Encrypt/Step1Config.cs b/Cryptomator/Commands/Encrypt/Step1Config.cs new file mode 100644 index 0000000..f3b46e0 --- /dev/null +++ b/Cryptomator/Commands/Encrypt/Step1Config.cs @@ -0,0 +1,46 @@ +using OneOf; + +namespace Cryptomator.Commands.Encrypt; + +public record Step1Config( + string PublicKeyPath, + string InputDirectory, + string OutputDirectory +) +{ + public static OneOf FromSettings(EncryptCommand.Settings settings) + { + + return new Step1Config( + settings.PublikKeyPath, + settings.InputDir, + settings.OutputDir + ); + } + + public static OneOf FromArgs(string[] args) + { + var pubkey = args[0]; + if (!File.Exists(pubkey)) + { + return "Pubkey not found"; + } + + var inputDir = args[1]; + + if (!Directory.Exists(inputDir)) + { + return "Input directory not found: " + inputDir; + } + + var outputDir = args[2]; + + if (!Directory.Exists(outputDir)) + { + return "Output directory not found: " + outputDir; + } + + var config = new Step1Config(pubkey, inputDir, outputDir); + return config; + } +} \ No newline at end of file diff --git a/Cryptomator/Commands/Encrypt/Step2Rsa.cs b/Cryptomator/Commands/Encrypt/Step2Rsa.cs new file mode 100644 index 0000000..d06bb08 --- /dev/null +++ b/Cryptomator/Commands/Encrypt/Step2Rsa.cs @@ -0,0 +1,42 @@ +using System.Security.Cryptography; +using OneOf; + +namespace Cryptomator.Commands.Encrypt; + +public sealed record Step2Rsa( + RSA Rsa, + string InputDirectory, + string OutputDirectory +) +{ + public static async Task> FromConfig(Step1Config step1Config) + { + var content = await File.ReadAllTextAsync(step1Config.PublicKeyPath); + try + { + var rsa = LoadPublicKeyFromPem(content); + var config2 = new Step2Rsa(rsa, step1Config.InputDirectory, step1Config.OutputDirectory); + return config2; + }catch(Exception e) + { + return e.Message; + } + } + + private static RSA LoadPublicKeyFromPem(string pemFileContent) + { + // Remove header and footer lines + // pemFileContent = pemFileContent.Replace("-----BEGIN PUBLIC KEY-----", "").Replace("-----END PUBLIC KEY-----", "").Replace("\n", ""); + + // Convert the PEM content to a byte array + // byte[] publicKeyBytes = Convert.FromBase64String(pemFileContent); + + // Create an RSA instance + RSA rsa = RSA.Create(); + rsa.ImportFromPem(pemFileContent); + // Import the public key + // rsa.ImportSubjectPublicKeyInfo(publicKeyBytes, out _); + + return rsa; + } +} \ No newline at end of file diff --git a/Cryptomator/Commands/Encrypt/Step3Encrypt.cs b/Cryptomator/Commands/Encrypt/Step3Encrypt.cs new file mode 100644 index 0000000..0d317cc --- /dev/null +++ b/Cryptomator/Commands/Encrypt/Step3Encrypt.cs @@ -0,0 +1,51 @@ +using System.Security.Cryptography; +using OneOf; +using OneOf.Types; + +namespace Cryptomator.Commands.Encrypt; + +public class Step3Encrypt +{ + public static async Task> FromStep2(Step2Rsa step2Rsa) + { + var files = Directory.GetFiles(step2Rsa.InputDirectory); + await Parallel.ForEachAsync(files, async (file, ct) => + { + try + { + var fi = new FileInfo(file); + using var aes = Aes.Create(); + aes.KeySize = 256; + aes.GenerateKey(); + aes.GenerateIV(); + var keyPath = Path.Combine(step2Rsa.OutputDirectory, $"{fi.Name}.key"); + var ivPath = Path.Combine(step2Rsa.OutputDirectory, $"{fi.Name}.iv"); + var encFile = Path.Combine(step2Rsa.OutputDirectory, $"{fi.Name}.enc"); + + var rsa = step2Rsa.Rsa; + var encKey = rsa.Encrypt(aes.Key, RSAEncryptionPadding.OaepSHA512); + await File.WriteAllBytesAsync(keyPath, encKey, ct); + + var encIv = rsa.Encrypt(aes.IV, RSAEncryptionPadding.OaepSHA512); + await File.WriteAllBytesAsync(ivPath, encIv, ct); + + await using var fs = File.OpenRead(file); + await using var encFs = File.OpenWrite(encFile); + await using var encStream = new CryptoStream( + encFs, + aes.CreateEncryptor(), + CryptoStreamMode.Write + ); + await fs.CopyToAsync(encStream, ct); + await encStream.FlushAsync(ct); + + Console.WriteLine($"Encrypted {fi.Name}"); + } + catch (Exception e) + { + Console.WriteLine(e); + } + }); + return new None(); + } +} \ No newline at end of file diff --git a/Cryptomator/Cryptomator.csproj b/Cryptomator/Cryptomator.csproj new file mode 100644 index 0000000..aeec73a --- /dev/null +++ b/Cryptomator/Cryptomator.csproj @@ -0,0 +1,15 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + diff --git a/Cryptomator/Program.cs b/Cryptomator/Program.cs new file mode 100644 index 0000000..517d93a --- /dev/null +++ b/Cryptomator/Program.cs @@ -0,0 +1,21 @@ +// See https://aka.ms/new-console-template for more information + +using Cryptomator.Commands; +using Cryptomator.Commands.Encrypt; +using Spectre.Console.Cli; + +var app = new CommandApp(); +app.Configure(config => +{ + config.AddCommand("encrypt"); + config.AddCommand("decrypt"); + config.AddCommand("create-keypair"); +}); +await app.RunAsync(args); + + + + + + + diff --git a/Cryptomator/RoP.cs b/Cryptomator/RoP.cs new file mode 100644 index 0000000..5bf3f38 --- /dev/null +++ b/Cryptomator/RoP.cs @@ -0,0 +1,58 @@ +using OneOf; + +namespace Cryptomator; + +public static class RoP +{ + public static OneOf ContinueWith(this OneOf value, Func> func) + { + var result = value.Match( + func, + err => err + ); + + return result; + } + + public static async Task> ContinueWith( + this OneOf value, + Func>> func + ) + { + var result = await value.Match( + func, + err => Task.FromResult>(err) + ); + + return result; + } + + public static async Task> ContinueWith( + this Task> value, + Func>> func + ) + { + var valRes = await value; + var result = await valRes.Match( + func, + err => Task.FromResult>(err) + ); + + return result; + } + + public static async Task> ContinueWith( + this Task> value, + Func> func + ) + { + var valRes = await value; + var result = valRes.Match( + func, + err => err + ); + + return result; + } + +} \ No newline at end of file