initial commit

This commit is contained in:
Robin Ehlert
2025-04-21 20:47:08 +02:00
commit 6492cd7903
11 changed files with 396 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
bin/
obj/
.idea/
*.user

16
Cryptomator.sln Normal file
View File

@@ -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

View File

@@ -0,0 +1,24 @@
using System.Security.Cryptography;
using Spectre.Console.Cli;
namespace Cryptomator.Commands;
public sealed class CreateKeyPair: AsyncCommand<CreateKeyPair.Settings>
{
public sealed class Settings : CommandSettings
{
[CommandArgument(0, "<./>")]
public string Path { get; set; }
}
public override async Task<int> 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;
}
}

View File

@@ -0,0 +1,77 @@
using System.Security.Cryptography;
using Spectre.Console.Cli;
namespace Cryptomator.Commands;
public sealed class Decrypt: AsyncCommand<Decrypt.Settings>
{
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<int> 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<RSA> 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);
}
}

View File

@@ -0,0 +1,42 @@
using System.ComponentModel;
using Spectre.Console.Cli;
namespace Cryptomator.Commands.Encrypt;
public sealed class EncryptCommand : AsyncCommand<EncryptCommand.Settings>
{
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<int> 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;
}
}

View File

@@ -0,0 +1,46 @@
using OneOf;
namespace Cryptomator.Commands.Encrypt;
public record Step1Config(
string PublicKeyPath,
string InputDirectory,
string OutputDirectory
)
{
public static OneOf<Step1Config, string> FromSettings(EncryptCommand.Settings settings)
{
return new Step1Config(
settings.PublikKeyPath,
settings.InputDir,
settings.OutputDir
);
}
public static OneOf<Step1Config, string> 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;
}
}

View File

@@ -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<OneOf<Step2Rsa, string>> 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;
}
}

View File

@@ -0,0 +1,51 @@
using System.Security.Cryptography;
using OneOf;
using OneOf.Types;
namespace Cryptomator.Commands.Encrypt;
public class Step3Encrypt
{
public static async Task<OneOf<None, string>> 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();
}
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OneOf" Version="3.0.271" />
<PackageReference Include="Spectre.Console.Cli" Version="0.50.0" />
</ItemGroup>
</Project>

21
Cryptomator/Program.cs Normal file
View File

@@ -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<EncryptCommand>("encrypt");
config.AddCommand<Decrypt>("decrypt");
config.AddCommand<CreateKeyPair>("create-keypair");
});
await app.RunAsync(args);

58
Cryptomator/RoP.cs Normal file
View File

@@ -0,0 +1,58 @@
using OneOf;
namespace Cryptomator;
public static class RoP
{
public static OneOf<TRes, TErr> ContinueWith<TOk, TErr, TRes>(this OneOf<TOk, TErr> value, Func<TOk, OneOf<TRes, TErr>> func)
{
var result = value.Match(
func,
err => err
);
return result;
}
public static async Task<OneOf<TRes, TErr>> ContinueWith<TOk, TErr, TRes>(
this OneOf<TOk, TErr> value,
Func<TOk, Task<OneOf<TRes, TErr>>> func
)
{
var result = await value.Match(
func,
err => Task.FromResult<OneOf<TRes, TErr>>(err)
);
return result;
}
public static async Task<OneOf<TRes, TErr>> ContinueWith<TOk, TErr, TRes>(
this Task<OneOf<TOk, TErr>> value,
Func<TOk, Task<OneOf<TRes, TErr>>> func
)
{
var valRes = await value;
var result = await valRes.Match(
func,
err => Task.FromResult<OneOf<TRes, TErr>>(err)
);
return result;
}
public static async Task<OneOf<TRes, TErr>> ContinueWith<TOk, TErr, TRes>(
this Task<OneOf<TOk, TErr>> value,
Func<TOk, OneOf<TRes, TErr>> func
)
{
var valRes = await value;
var result = valRes.Match(
func,
err => err
);
return result;
}
}