diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b468a4d..94bf695 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,12 +11,18 @@ jobs: include: - os: windows-latest target: win-x86 + - os: windows-latest + target: win-x64 - os: windows-latest target: win-arm + - os: windows-latest + target: win-arm64 - os: ubuntu-latest target: linux-x64 - os: ubuntu-latest target: linux-arm + - os: ubuntu-latest + target: linux-arm64 - os: macos-latest target: osx-x64 - os: macos-latest @@ -27,8 +33,6 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: 6.0.x - - name: Restore dependencies - run: dotnet restore - name: Build run: dotnet publish -p:PublishSingleFile=true -r ${{ matrix.target }} -c Release --self-contained true -p:PublishTrimmed=true -p:EnableCompressionInSingleFile=true - name: Test diff --git a/CliUtils.cs b/CliUtils.cs new file mode 100644 index 0000000..08c8247 --- /dev/null +++ b/CliUtils.cs @@ -0,0 +1,27 @@ +using System.Linq; + +namespace SafeFolder; + +public record Flag(string FullName, string? CompactName); + +public static class CliUtils { + public static bool hasFlag(string[] args, Flag flag) { + string full = "--" + flag.FullName; + string? small = flag.CompactName is not null ? "-" + flag.CompactName : null; + return args.Any(x => x == full || ( small is not null && x == small )); + } + + public static string? getFlagValue(string[] args, Flag flag) { + string full = "--" + flag.FullName; + string? small = flag.CompactName is not null ? "-" + flag.CompactName : null; + + for (var i = 0; i < args.Length; i++) { + if (args[i] != full && args[i] != small) + continue; + if (i + 1 < args.Length) { + return args[i + 1]; + } + } + return null; + } +} \ No newline at end of file diff --git a/Encryptor.cs b/Encryptor.cs deleted file mode 100644 index bbea220..0000000 --- a/Encryptor.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.IO; -using System.Security.Cryptography; - -namespace SafeFolder{ - public static class Encryptor - { - private static readonly int _keySize = 256; - private static readonly int _blockSize = 128; - private static readonly PaddingMode _paddingMode = PaddingMode.PKCS7; - private static readonly CipherMode _cipherMode = CipherMode.CBC; - - internal static void AesFileEncrypt(string inputFile, string outputFile, byte[] key,byte[] iv){ - var cryptFile = outputFile ?? throw new ArgumentNullException(nameof(outputFile)); - using var fsCrypt = new FileStream(cryptFile, FileMode.Create); - - using var aes = Aes.Create(); - aes.KeySize = _keySize; - aes.BlockSize = _blockSize; - aes.Padding = _paddingMode; - aes.Mode = _cipherMode; - aes.Key = key; - aes.IV = iv; - - using var cs = new CryptoStream(fsCrypt, - aes.CreateEncryptor(), - CryptoStreamMode.Write); - - using var fsIn = new FileStream(inputFile, FileMode.Open); - - int data; - while ((data = fsIn.ReadByte()) != -1) - cs.WriteByte((byte)data); - } - - internal static void AesFileDecrypt(string inputFile, string outputFile, byte[] key,byte[] iv){ - using var fsCrypt = new FileStream(inputFile, FileMode.Open); - - using var aes = Aes.Create(); - aes.KeySize = _keySize; - aes.BlockSize = _blockSize; - aes.Padding = _paddingMode; - aes.Mode = _cipherMode; - aes.Key = key; - aes.IV = iv; - - using var cs = new CryptoStream(fsCrypt, - aes.CreateDecryptor(), - CryptoStreamMode.Read); - - using var fsOut = new FileStream(outputFile, FileMode.Create); - - int data; - while ((data = cs.ReadByte()) != -1) - fsOut.WriteByte((byte)data); - } - - public static string AesEncryptString(string plainText, byte[] key, byte[] iv) - { - // Check arguments. - if (plainText is not { Length: > 0 }) - throw new ArgumentNullException(nameof(plainText)); - if (key is not { Length: > 0 }) - throw new ArgumentNullException(nameof(key)); - if (iv is not { Length: > 0 }) - throw new ArgumentNullException(nameof(iv)); - - // Create an AesManaged object - // with the specified key and IV. - using var aesAlg = Aes.Create(); - aesAlg.Key = key; - aesAlg.IV = iv; - - // Create an encryptor to perform the stream transform. - var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); - - // Create the streams used for encryption. - using var msEncrypt = new MemoryStream(); - using var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write); - using var swEncrypt = new StreamWriter(csEncrypt); - //Write all data to the stream. - swEncrypt.Write(plainText); - var encrypted = msEncrypt.ToArray(); - - // Return the encrypted bytes from the memory stream. - return Convert.ToBase64String(encrypted); - } - - public static string AesDecryptString(string cipherText, byte[] key, byte[] iv) - { - // Check arguments. - if (cipherText is not { Length: > 0 }) - throw new ArgumentNullException(nameof(cipherText)); - if (key is not { Length: > 0 }) - throw new ArgumentNullException(nameof(key)); - if (iv is not { Length: > 0 }) //black magic - throw new ArgumentNullException(nameof(iv)); - - // Declare the string used to hold - // the decrypted text. - - // Create an AesManaged object - // with the specified key and IV. - // was using AesManaged aesAlg = new AesManaged(); - using var aesAlg = Aes.Create(); - aesAlg.Key = key; - aesAlg.IV = iv; - - // Create a decryptor to perform the stream transform. - var decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); - - var cipherTextBytes = Convert.FromBase64String(cipherText); - - // Create the streams used for decryption. - using var msDecrypt = new MemoryStream(cipherTextBytes); - using var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read); - using var srDecrypt = new StreamReader(csDecrypt); - // Read the decrypted bytes from the decrypting stream - // and place them in a string. - var plaintext = srDecrypt.ReadToEnd(); - - return plaintext; - } - - } -} \ No newline at end of file diff --git a/Engine.cs b/Engine.cs index acccb7a..8fef852 100644 --- a/Engine.cs +++ b/Engine.cs @@ -1,128 +1,343 @@ -using System; +using PerrysNetConsole; +using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; -using System.IO.Compression; using System.Linq; +using System.Security.Cryptography; using System.Threading.Tasks; +using System.IO.Compression; namespace SafeFolder; public static class Engine { - #region Folders + private const int KeySize = 256; + private const int BlockSize = 128; + private const PaddingMode PaddingMode = System.Security.Cryptography.PaddingMode.PKCS7; + private const CipherMode CipherMode = System.Security.Cryptography.CipherMode.CBC; + + private static readonly string _safeFolderName = Process.GetCurrentProcess().ProcessName; - public static async Task PackFolders(byte[] key) + #region Files + + private static void PackSingleFile(byte[] key, string pwdHash, string file) { - // for each folder, compress, encrypt, delete folder - var folders = Utils.GetFoldersFromSafeFile(); - var taskList = new List(); + #region header + var iv = Utils.GenerateIv(); + var guid = Guid.NewGuid(); + var encFile = guid.ToString().Replace("-", "") + ".enc"; + var header = new Header{ + Guid = guid, + Hash = pwdHash, + Name = file, + IvLength = iv.Length, + Iv = iv + }; + #endregion + + #region stream init + using var outStream = File.Create(encFile); + using var inStream = File.OpenRead(file); + using var bw = new BinaryWriter(outStream); + bw.Write(header); + #endregion - foreach (var folder in folders) + #region cryptography + using var aes = Aes.Create(); + aes.KeySize = KeySize; + aes.BlockSize = BlockSize; + aes.Padding = PaddingMode; + aes.Mode = CipherMode; + aes.Key = key; + aes.IV = iv; + + using var cryptoStream = new CryptoStream(outStream, aes.CreateEncryptor(), CryptoStreamMode.Write); + Span buffer = stackalloc byte[1024]; + int bytesRead; + while ( (bytesRead= inStream.Read(buffer)) > 0) { - taskList.Add(Task.Run(() => - { - var dirInfo = new DirectoryInfo(folder); - var zipName = $"./{dirInfo.Name}.zip"; - ZipFile.CreateFromDirectory(dirInfo.FullName, zipName, CompressionLevel.Fastest, false); - Encryptor.AesFileEncrypt(zipName, zipName + ".enc", key, Utils.GetIvFromSafeFile()); - File.Delete(zipName); - Directory.Delete(folder, true); - })); - } - var whenAllTask = Task.WhenAll(taskList); - try{ - await whenAllTask; - } - catch{ - whenAllTask.Exception.InnerExceptions.ToList() - .ForEach(e => Utils.WriteLine(e.Message, ConsoleColor.Red)); + cryptoStream.Write(buffer[..bytesRead]); } + + #endregion } - - public static async Task UnpackFolders(byte[] key) + private static void PackSingleFolder(byte[] key, string pwdHash, string folder, bool method) { - // for each folder, decrypt, decompress and delete zip - var folders = Utils.GetFoldersFromSafeFile(); - var taskList = new List(); + if (method){ + var dirInfo = new DirectoryInfo(folder); + using var ms = new MemoryStream(); + using var zip = new Ionic.Zip.ZipFile(encoding: System.Text.Encoding.UTF8); - foreach (var folder in folders) - { - taskList.Add(Task.Run(() => + zip.AddDirectory(folder, dirInfo.Name); + zip.Save(ms); + ms.Seek(0, SeekOrigin.Begin); + + + #region header + var iv = Utils.GenerateIv(); + var guid = Guid.NewGuid(); + var encFile = guid.ToString().Replace("-", "") + ".enc"; + var header = new Header{ + IsFolder = true, + Guid = guid, + Hash = pwdHash, + Name = dirInfo.Name, + IvLength = iv.Length, + Iv = iv + }; + + #endregion + + #region stream init + using var outStream = File.Create(encFile); + using var bw = new BinaryWriter(outStream); + bw.Write(header); + #endregion + + #region cryptography + using var aes = Aes.Create(); + aes.KeySize = KeySize; + aes.BlockSize = BlockSize; + aes.Padding = PaddingMode; + aes.Mode = CipherMode; + aes.Key = key; + aes.IV = iv; + + using var cryptoStream = new CryptoStream(outStream, aes.CreateEncryptor(), CryptoStreamMode.Write); + Span buffer = stackalloc byte[1024]; + int bytesRead; + while ( (bytesRead= ms.Read(buffer)) > 0) { - var dirInfo = new DirectoryInfo(folder); - var zipName = $"./{dirInfo.Name}.zip"; - var zipEncName = $"./{dirInfo.Name}.zip.enc"; - Encryptor.AesFileDecrypt(zipEncName, zipName, key, Utils.GetIvFromSafeFile()); - ZipFile.ExtractToDirectory(zipName, dirInfo.FullName); - File.Delete(zipEncName); - File.Delete(zipName); - })); - } + cryptoStream.Write(buffer[..bytesRead]); + } - var whenAllTask = Task.WhenAll(taskList); - try{ - await whenAllTask; - } - catch{ - whenAllTask.Exception.InnerExceptions.ToList() - .ForEach(e => Utils.WriteLine(e.Message, ConsoleColor.Red)); + #endregion + }else{ + var dirInfo = new DirectoryInfo(folder); + var zipName = $"./{dirInfo.Name}.zip"; + ZipFile.CreateFromDirectory(dirInfo.FullName, zipName, CompressionLevel.Fastest, true); + + var iv = Utils.GenerateIv(); + var guid = Guid.NewGuid(); + var encFile = guid.ToString().Replace("-", "") + ".enc"; + var header = new Header{ + IsFolder = true, + Guid = guid, + Hash = pwdHash, + Name = dirInfo.Name, + IvLength = iv.Length, + Iv = iv + }; + + using var outStream = File.Create(encFile); + using var inStream = File.OpenRead(zipName); + using var bw = new BinaryWriter(outStream); + + bw.Write(header); + + using var aes = Aes.Create(); + aes.KeySize = KeySize; + aes.BlockSize = BlockSize; + aes.Padding = PaddingMode; + aes.Mode = CipherMode; + aes.Key = key; + aes.IV = iv; + + using var cryptoStream = new CryptoStream(outStream, aes.CreateEncryptor(), CryptoStreamMode.Write); + Span buffer = stackalloc byte[1024]; + int bytesRead; + while ( (bytesRead= inStream.Read(buffer)) > 0) + { + cryptoStream.Write(buffer[..bytesRead]); + } } } + + #pragma warning disable CS8602 + public static async Task PackFiles(byte[] key, string pwdHash, Progress? prog, bool method, bool traces) { + bool verbose = prog is not null; + List files = Directory.EnumerateFiles(Directory.GetCurrentDirectory()) + .Where(f => !Path.GetFileName(f).Contains(_safeFolderName) && !f.EndsWith(".pdb") && !f.EndsWith(".enc")) + .ToList(); - #endregion + List folders = Directory.GetDirectories(Directory.GetCurrentDirectory()).ToList(); - #region Files + double progress = 100.0 / (files.Count + folders.Count == 0 ? 100 : files.Count + folders.Count); - public static async Task PackFiles(byte[] key) - { - // files don't need to be compressed, just encrypted - var files = Utils.GetFilesFromSafeFile(); - var taskList = new List(); - - foreach (var file in files) + // encrypt files + await Parallel.ForEachAsync(files, (file, _) => { - taskList.Add(Task.Run(() => - { - if (file.EndsWith(".safe")) return; - Encryptor.AesFileEncrypt(file, file + ".enc", key, Utils.GetIvFromSafeFile()); + try{ + PackSingleFile(key, pwdHash, Path.GetFileName(file)); + prog?.Message(Message.LEVEL.DEBUG, $"{Path.GetFileName(file)} encrypted successfully"); + if (traces){ + Utils.WipeFile(file, prog); + }else{ File.Delete(file); - })); - } + } + if(verbose) prog.Percentage += progress; + }catch (Exception e) + { + prog?.Message(Message.LEVEL.ERROR, $"{e.Message}"); + prog?.Stop(); + Console.WriteLine(e); + } - var whenAllTask = Task.WhenAll(taskList); - try{ - await whenAllTask; - } - catch{ - whenAllTask.Exception.InnerExceptions.ToList() - .ForEach(e => Utils.WriteLine(e.Message, ConsoleColor.Red)); - } + return ValueTask.CompletedTask; + }); + + // encrypt folders + await Parallel.ForEachAsync(folders, (folder, _) => + { + try{ + PackSingleFolder(key, pwdHash, Path.GetFileName(folder), method); + prog?.Message(Message.LEVEL.DEBUG, $"{Path.GetFileName(folder)} encrypted successfully"); + if (traces){ + Utils.WipeFolder(folder, prog); + Utils.WipeFile($"{folder}.zip", prog); + }else{ + Directory.Delete(folder, true); + File.Delete(Path.GetFileName(folder) + ".zip"); + } + if(verbose) prog.Percentage += progress; + }catch (Exception e) + { + prog?.Message(Message.LEVEL.ERROR, $"{e.Message}"); + prog?.Stop(); + Console.WriteLine(e); + } + + return ValueTask.CompletedTask; + }); } - - public static async Task UnpackFiles(byte[] key) - { - // files don't need to be decompressed, just decrypted - var files = Utils.GetFilesFromSafeFile(); - var taskList = new List(); - foreach (var file in files) + private static void UnpackSingleFileOrFolder(byte[] key, string file, Progress? prog, bool method, bool traces) + { + #region header + var guidFileName = Guid.Parse(Path.GetFileName(file).Replace(".enc", "")); + using var inStream = new FileStream(file, FileMode.Open, FileAccess.Read); + using var br = new BinaryReader(inStream); + var header = br.ReadHeader(); + if(header.Guid != guidFileName || !Utils.CheckHash(Utils.HashBytes(key), header.Hash)) { - taskList.Add(Task.Run(() => - { - if (file.EndsWith(".safe")) return; - Encryptor.AesFileDecrypt(file + ".enc", file, key, Utils.GetIvFromSafeFile()); - File.Delete(file + ".enc"); - })); + prog?.Message(Message.LEVEL.WARN, $"Wrong password or file corrupted ({Path.GetFileName(file)})"); + return; } - var whenAllTask = Task.WhenAll(taskList); - try{ - await whenAllTask; + + var isFolder = header.IsFolder; + #endregion + + #region criptography + + if (!isFolder) + { + // file + using var outStream = File.Create(header.Name); + using var aes = Aes.Create(); + aes.KeySize = KeySize; + aes.BlockSize = BlockSize; + aes.Padding = PaddingMode; + aes.Mode = CipherMode; + aes.Key = key; + aes.IV = header.Iv; + + using var cryptoStream = new CryptoStream(inStream, aes.CreateDecryptor(), CryptoStreamMode.Read); + Span buffer = stackalloc byte[1024]; + int bytesRead; + while ((bytesRead = cryptoStream.Read(buffer)) > 0) + outStream.Write(buffer[..bytesRead]); + + inStream.Close(); } - catch{ - whenAllTask.Exception.InnerExceptions.ToList() - .ForEach(e => Utils.WriteLine(e.Message, ConsoleColor.Red)); + else + { + // directory + if (method){ + using var ms = new MemoryStream(); + using var aes = Aes.Create(); + aes.KeySize = KeySize; + aes.BlockSize = BlockSize; + aes.Padding = PaddingMode; + aes.Mode = CipherMode; + aes.Key = key; + aes.IV = header.Iv; + + using var cryptoStream = new CryptoStream(inStream, aes.CreateDecryptor(), CryptoStreamMode.Read); + Span buffer = stackalloc byte[1024]; + int bytesRead; + while ((bytesRead = cryptoStream.Read(buffer)) > 0) + ms.Write(buffer[..bytesRead]); + + ms.Seek(0, SeekOrigin.Begin); + + // ms has a zip file + using var zip = Ionic.Zip.ZipFile.Read(ms); + + + zip.ExtractAll(Directory.GetCurrentDirectory() , Ionic.Zip.ExtractExistingFileAction.OverwriteSilently); + }else{ + using var outStream = File.Create($"{header.Name}.zip"); + using var aes = Aes.Create(); + aes.KeySize = KeySize; + aes.BlockSize = BlockSize; + aes.Padding = PaddingMode; + aes.Mode = CipherMode; + aes.Key = key; + aes.IV = header.Iv; + + using var cryptoStream = new CryptoStream(inStream, aes.CreateDecryptor(), CryptoStreamMode.Read); + Span buffer = stackalloc byte[1024]; + int bytesRead; + while ((bytesRead = cryptoStream.Read(buffer)) > 0) + outStream.Write(buffer[..bytesRead]); + + inStream.Close(); + outStream.Close(); + + ZipFile.ExtractToDirectory($"{header.Name}.zip", "./"); + + if(traces){ + Utils.WipeFile($"{header.Name}.zip", prog); + }else{ + File.Delete($"{header.Name}.zip"); + } + } } + File.Delete(file); + prog?.Message(Message.LEVEL.DEBUG, $"{Path.GetFileName(file)} decrypted successfully"); + + #endregion } + public static async Task UnpackFiles(byte[] key, string pwdHash, Progress? prog, bool method, bool traces) { + bool verbose = prog is not null; + List files = Directory.EnumerateFiles(Directory.GetCurrentDirectory()) + .Where(f => f.EndsWith(".enc")).ToList(); + + List zips = Directory.EnumerateFiles(Directory.GetCurrentDirectory()) + .Where(f => f.EndsWith(".zip")).ToList(); + + double progress = 100.0 / (!files.Any() ? 100 : files.Count + zips.Count); + + + // decrypt files and folders + await Parallel.ForEachAsync(files, (file, _) => + { + try{ + UnpackSingleFileOrFolder(key, file, prog, method, traces); + if(verbose) prog.Percentage += progress; + }catch (Exception e) + { + prog?.Message(Message.LEVEL.ERROR, $"{e.Message}"); + prog?.Stop(); + Console.WriteLine(e); + } + + return ValueTask.CompletedTask; + }); + } + #endregion -} \ No newline at end of file +} +#pragma warning restore CS8602 \ No newline at end of file diff --git a/Header.cs b/Header.cs index 12d6ebd..9cd6048 100644 --- a/Header.cs +++ b/Header.cs @@ -1,15 +1,26 @@ using System; -using System.IO; namespace SafeFolder; [Serializable] -public class Header -{ - public int size { get; set; } - public string hash { get; set; } - public string name { get; set; } - public Guid guid { get; set; } - public int ivLength { get; set; } - public byte[] iv { get; set; } +public class Header { + public string Hash { get; set; } = ""; + public bool IsFolder { get; set; } + public string Name { get; set; } = ""; + public Guid Guid { get; set; } = Guid.NewGuid(); + public int IvLength { get; set; } = 0; + public byte[] Iv { get; set; } = Array.Empty(); + + public Header(string hash, bool isFolder, string name, Guid guid, int ivLength, byte[] iv) { + Hash = hash; + IsFolder = isFolder; + Name = name; + Guid = guid; + IvLength = ivLength; + Iv = iv; + } + + public Header() { + + } } \ No newline at end of file diff --git a/Installer.cs b/Installer.cs deleted file mode 100644 index 307a6bc..0000000 --- a/Installer.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Diagnostics; - -namespace SafeFolder -{ - public static class Installer{ - - private static readonly string _currentPath = Environment.CurrentDirectory; - private static readonly string _safeFolderName = Process.GetCurrentProcess().ProcessName; - private static readonly string _safeFilePath = $"{_currentPath}/.safe"; - public static bool IsInstalled() => File.Exists(_safeFilePath); - - public static bool Install() - { - // delete old safe - if (File.Exists(_safeFilePath)) File.Delete(_safeFilePath); - // file will be binary - using var binaryWriter = new BinaryWriter(File.Open(_safeFilePath, FileMode.Create)); - - // prompt for password - var pwd = Utils.GetPasswordInput("Enter password: "); - var rePwd = Utils.GetPasswordInput("Re-enter password: "); - - if (pwd != rePwd) - { - Utils.WriteLine("Passwords do not match, exiting", ConsoleColor.Red); - return false; - } - - var pwdHash = Utils.GetHash(pwd); - // var finalHash = Utils.GetHash(pwdHash + pwd); - - // write current state - binaryWriter.Write(false); - - // write finalHash to safeFile - binaryWriter.Write(pwdHash); - - var files = GetFiles(); - var folders = GetFolders(); - - binaryWriter.Write(files); - binaryWriter.Write(folders); - - var iv = Utils.GenerateIv(); - binaryWriter.Write(iv.Length); - binaryWriter.Write(iv); - - File.SetAttributes(_safeFilePath, FileAttributes.Hidden); - - Console.WriteLine("Safe Folder Installed!"); - return true; - } - - public static IEnumerable GetFiles() => Directory.GetFiles(_currentPath) - .Where(f => !f.EndsWith(".safe") && !f.EndsWith(_safeFolderName) && !f.EndsWith($"{_safeFolderName}.exe")); - - public static IEnumerable GetFolders() => Directory.GetDirectories(_currentPath); - } -} \ No newline at end of file diff --git a/Program.cs b/Program.cs index 74e04e2..d29ab98 100644 --- a/Program.cs +++ b/Program.cs @@ -1,94 +1,167 @@ -using System; +using PerrysNetConsole; +using System; using System.Diagnostics; +using System.Reflection; using System.Threading.Tasks; +using Prompt = Sharprompt.Prompt; namespace SafeFolder; public static class Program { - private static async Task Main() - { - // get if .safe file is installed - if (!Installer.IsInstalled()) - { - Utils.WriteLine("SafeFolder is not installed. Installing now.", ConsoleColor.Yellow); - var canProceed = Installer.Install(); - if(!canProceed) { - Utils.WriteLine("SafeFolder installation failed.", ConsoleColor.Red); - Utils.WriteLine("Press any key to close...", ConsoleColor.Red); - return; - } - } - var hashFile = Utils.GetHashFromSafeFile(); + public struct ProgramOptions { + public bool IsNoGui { get; init; } + public bool HelpRequested { get; init; } + public bool VersionRequested { get; init; } + public bool InMemory { get; init; } + public bool ClearTraces { get; init; } + public bool Encrypt { get; init; } + public bool Decrypt { get; init; } + public string? Password { get; init; } + public int Verbosity { get; init; } + } + private static async Task Main(string[] args) { + bool isNoGui = CliUtils.hasFlag(args, new Flag("nogui", "n")); + bool helpRequested = CliUtils.hasFlag(args, new Flag("help", "h")); + bool versionRequested = CliUtils.hasFlag(args, new Flag("version", "v")); + bool inMemory = CliUtils.hasFlag(args, new Flag("inmemory", "m")); + bool clearTraces = CliUtils.hasFlag(args, new Flag("cleartraces", "c")); + bool encrypt = CliUtils.hasFlag(args, new Flag("encrypt", "e")); + bool decrypt = CliUtils.hasFlag(args, new Flag("decrypt", "d")); + string? password = CliUtils.getFlagValue(args, new Flag("password", "p")); + string? verbosity = CliUtils.getFlagValue(args, new Flag("verbosity", "V")); - if (string.IsNullOrWhiteSpace(hashFile)) - { - if (!Utils.ShowCorrupt()) return; - hashFile = Utils.GetHashFromSafeFile(); - } + ProgramOptions opt = new() { + IsNoGui = isNoGui, + HelpRequested = helpRequested, + VersionRequested = versionRequested, + InMemory = inMemory, + ClearTraces = clearTraces, + Encrypt = encrypt, + Decrypt = decrypt, + Password = password, + Verbosity = verbosity == null ? 0 : int.Parse(verbosity) + }; + - Utils.ShowSplashScreen(); + if (isNoGui) + await StartCli(opt); + else + await StartInteractive(); + } + + private static async Task StartInteractive() { + var method = false; + var traces = false; - var pwd = Utils.GetPasswordInput("Enter password: "); - - var isValid = BCrypt.Net.BCrypt.Verify(pwd, hashFile); - if(!isValid){ - Utils.WriteLine("Invalid password.", ConsoleColor.Red); + Utils.ShowSplashScreen(); - const int maxTry = 2; // 3 but user already used once - var tryCount = 0; - while (tryCount < maxTry) - { - pwd = Utils.GetPasswordInput("Enter password: "); - // pwdHash = Utils.GetHash(pwd); - // finalHash = Utils.GetHash(pwdHash + pwd); - isValid = BCrypt.Net.BCrypt.Verify(pwd, hashFile); - if (isValid) break; - Utils.WriteLine("Invalid password.", ConsoleColor.Red); - tryCount++; - } - } - - if (!isValid) + string? state = Prompt.Select("What do you want", new[] { "Encrypt Files", "Decrypt Files", "Info about program" }); + switch (state) { - Utils.WriteLine("Too many invalid password attempts. Press any key to close.", ConsoleColor.Red); - Console.ReadKey(true); - return; + case "Info about program": + Utils.WriteLine("SafeFolder\n"); + Utils.ShowInfoScreen(); + Console.WriteLine("Press any key to close the program."); + Console.ReadKey(); + return; + case "Encrypt Files": + method = Prompt.Confirm("Encrypt files in memory? (Fast, but demands more ram)"); + traces = Prompt.Confirm("Clear traces? (Very Slow, but more secure)"); + break; + case "Decrypt Files": + method = Prompt.Confirm("Decrypt files in memory? (Fast, but demands more ram)"); + traces = Prompt.Confirm("Clear traces? (Very Slow, but more secure)"); + break; } - - // here we go - //from here, the password is correct - + var stopWatch = new Stopwatch(); - stopWatch.Start(); + var prog = new Progress(); - var state = Utils.GetStateFromSafeFile(); - var key = Utils.CreateKey(hashFile, pwd); - if (!state) - { - // have to encrypt - Utils.WriteLine("Encrypting files...", ConsoleColor.Green); - Utils.SetFilesToSafeFile(); - await Engine.PackFiles(key); - await Engine.PackFolders(key); + var pwd = Prompt.Password("Enter password", placeholder: "Take Care With CAPS-LOCK", validators: new[] { Sharprompt.Validators.Required(), Sharprompt.Validators.MinLength(4) }); + var pwd2 = Prompt.Password("Re-Enter password", placeholder: "Take Care With CAPS-LOCK", validators: new[] { Sharprompt.Validators.Required(), Sharprompt.Validators.MinLength(4) }); + if (pwd != pwd2) + throw new Exception("Passwords do not match"); - Utils.SetStateToSafeFile(true); + var key = Utils.DeriveKeyFromString(pwd); + var pwdHash = Utils.GetHash(Utils.HashBytes(key)); + switch (state) + { + case "Encrypt Files": + // have to encrypt + prog.Start(); + prog.Message(Message.LEVEL.INFO, "Encrypting files..."); + stopWatch.Start(); + await Engine.PackFiles(key, pwdHash, prog, method, traces); + break; + case "Decrypt Files": + // have to decrypt + prog.Start(); + prog.Message(Message.LEVEL.INFO, "Decrypting files..."); + stopWatch.Start(); + await Engine.UnpackFiles(key, pwdHash, prog, method, traces); + break; } - else{ - // have to decrypt - Utils.WriteLine("Decrypting files...", ConsoleColor.Green); - await Engine.UnpackFiles(key); - await Engine.UnpackFolders(key); - Utils.SetStateToSafeFile(false); - } stopWatch.Stop(); - var ms = stopWatch.Elapsed.Milliseconds; - var s = stopWatch.Elapsed.Seconds; - var m = stopWatch.Elapsed.Minutes; + var ms = stopWatch.Elapsed.Milliseconds.ToString("D3"); + var s = stopWatch.Elapsed.Seconds.ToString("D2"); + var m = stopWatch.Elapsed.Minutes.ToString("D2"); - Utils.WriteLine($"Done in {m}:{s}:{ms}!", ConsoleColor.Green); + prog.Message(Message.LEVEL.SUCCESS, $"Done in {m}:{s}:{ms}!"); + prog.Update(100); + prog.Stop(); + CoEx.WriteLine(); Console.WriteLine("Press any key to close the program."); Console.ReadKey(); } + + private static async Task StartCli(ProgramOptions opt) { + if (opt.HelpRequested) { + Utils.ShowInfoScreen(); + return true; + } + if (opt.VersionRequested) { + string version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "Unknown"; + Utils.WriteLine($"Current SafeFolder version: {version}", version == "Unknown" ? ConsoleColor.Red : ConsoleColor.Green); + return true; + } + if(!opt.Decrypt && !opt.Encrypt) { + Utils.WriteLine("You must specify either --encrypt(-e) or --decrypt(-d)", ConsoleColor.Red); + return false; + } + if(opt.Decrypt && opt.Encrypt) { + Utils.WriteLine("You can't specify both --encrypt(-e) and --decrypt(-d)", ConsoleColor.Red); + return false; + } + if(opt.Password == null) { + Utils.WriteLine("You must specify a password using '--password ' or '-p '", ConsoleColor.Red); + return false; + } + if(opt.Password.Length < 4) { + Utils.WriteLine("Password must be at least 4 characters long", ConsoleColor.Red); + return false; + } + bool verbose = opt.Verbosity > 0; + Stopwatch stopWatch = new(); + stopWatch.Start(); + + byte[] key = Utils.DeriveKeyFromString(opt.Password); + string pwdHash = Utils.GetHash(Utils.HashBytes(key)); + if(verbose) + Utils.WriteLine($"{(opt.Encrypt ? "Encrypting" : "Decrypting")} files now"); + if (opt.Encrypt) { + await Engine.PackFiles(key, pwdHash, null, opt.InMemory, opt.ClearTraces); + }else { + await Engine.UnpackFiles(key, pwdHash, null, opt.InMemory, opt.ClearTraces); + } + + stopWatch.Stop(); + var ms = stopWatch.Elapsed.Milliseconds.ToString("D3"); + var s = stopWatch.Elapsed.Seconds.ToString("D2"); + var m = stopWatch.Elapsed.Minutes.ToString("D2"); + if (verbose) + Utils.WriteLine($"Done in {m}:{s}:{ms}!"); + return true; + } } \ No newline at end of file diff --git a/SafeFolder.csproj b/SafeFolder.csproj index d293cfb..2831c6e 100644 --- a/SafeFolder.csproj +++ b/SafeFolder.csproj @@ -3,8 +3,16 @@ Exe net6.0 + link + enable + 2.0 + 2.0 + en + + + diff --git a/Utils.cs b/Utils.cs index 7983698..b05204d 100644 --- a/Utils.cs +++ b/Utils.cs @@ -1,9 +1,8 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Security.Cryptography; -using System.Threading; +using System.Text; +using PerrysNetConsole; namespace SafeFolder { @@ -11,36 +10,6 @@ public static class Utils{ #region IO - /// - /// Shows a prompt and gets a string from the user(formatted with *). - /// - /// The text to be shown - /// The input the user typed - public static string GetPasswordInput(string prompt = "") - { - Console.Write(prompt); - var password = ""; - ConsoleKeyInfo key; - do - { - key = Console.ReadKey(true); - if (key.Key != ConsoleKey.Backspace && key.Key != ConsoleKey.Enter) - { - password += key.KeyChar; - Console.Write("*"); - } - else - { - if (key.Key != ConsoleKey.Backspace || password.Length <= 0) continue; - password = password[..^1]; // black magic, but it works - Console.Write("\b \b"); - } - } - while (key.Key != ConsoleKey.Enter); - Console.WriteLine(); - return password; - } - /// /// Shows the splash screen. /// @@ -55,43 +24,33 @@ Welcome to SafeFolder ============================================= "); } - + /// - /// Shows the app corrupt message and reinstall the app. + /// Shows the info screen. /// - /// - public static bool ShowCorrupt() + public static void ShowInfoScreen() { Console.WriteLine(@" -============================================= - - _ - .' ) - ,.--. / .' Installation is corrupted -// \ / / -\\ / / / - `'--' . ' - ,.--. | | -// \' ' -\\ / \ \ Try reinstalling SafeFolder - `'--' \ \ - \ '. - '._) - +Encrypt/Decrypt files in memory: Fast, but demands more ram. -============================================= +Encrypt/Decrypt files in memory: + - Uses the RAM memory to convert your files faster. Not recommended for big files as it might crash! +Clear traces: + - Clear all traces of the files in the hard drive, making it impossible to recover them. + +Safe folder now has full CLI support! Flags are now available to use in the command line: + -n --nogui => Disable the GUI and use the command line interface. Must be included to use the CLI flags. + -h --help => Show this help screen. + -v --version => Display the current version of the program. + -m --inmemory => Encrypt/Decrypt files in memory. + -c --cleartraces => Clear traces of the files in the hard drive. + -e --encrypt => Encrypt the files. + -d --decrypt => Decrypt the files. + -p --password => Set the password to use. + -V --verbosity <0|1> => Sets the verbosity level of the program. "); - WriteLine("Press any key to Reinstall...", ConsoleColor.Yellow); - Console.ReadKey(true); - var canProceed = Installer.Install(); - if(!canProceed) { - Utils.WriteLine("SafeFolder installation failed.", ConsoleColor.Red); - Utils.WriteLine("Press any key to close...", ConsoleColor.Red); - return false; - } - return true; } - + /// /// Writes a line to the console, with a color. /// @@ -110,65 +69,19 @@ public static void WriteLine(string message, ConsoleColor color = ConsoleColor.W #region Binary - /// - /// Writes a list of strings to a binary stream(writable) - /// - /// The binaryWriter object - /// The list containing the strings - public static void Write(this BinaryWriter stream, IEnumerable strings) - { - // Write the number of strings - var stringsList = strings.ToList(); - stream.Write(stringsList.Count); - - // Write each string - foreach (var str in stringsList) - { - stream.Write(str); - } - } - - /// - /// Reads a list of strings written by - /// - /// The stream containing the data - /// An IEnumerable with the strings read - private static IEnumerable ReadStrings(this BinaryReader stream) - { - var strings = new List(); - // Read the number of strings - var count = stream.ReadInt32(); - - // Read each string - for (var i = 0; i < count; i++) - { - strings.Add(stream.ReadString()); - } - - return strings; - } - /// /// Writes a GUID bytes to a binary stream /// /// The binary stream /// The to be written - private static void Write(this BinaryWriter stream, Guid guid) - { - var bytes = guid.ToByteArray(); - stream.Write(bytes); - } + private static void Write(this BinaryWriter stream, Guid guid) => stream.Write(guid.ToByteArray()); /// /// Reads a guid from a binary stream /// /// The binary stream /// The guid that has been read - private static Guid ReadGuid(this BinaryReader stream) - { - var bytes = stream.ReadBytes(16); - return new Guid(bytes); - } + private static Guid ReadGuid(this BinaryReader stream) => new(stream.ReadBytes(16)); /// /// Writes the file header to the stream @@ -177,12 +90,12 @@ private static Guid ReadGuid(this BinaryReader stream) /// The header object public static void Write(this BinaryWriter writer, Header header) { - writer.Write(header.size); - writer.Write(header.hash); - writer.Write(header.name); - writer.Write(header.guid); - writer.Write(header.ivLength); - writer.Write(header.iv); + writer.Write(header.Hash); + writer.Write(header.IsFolder); + writer.Write(header.Name); + writer.Write(header.Guid); + writer.Write(header.IvLength); + writer.Write(header.Iv); } /// @@ -194,128 +107,158 @@ public static Header ReadHeader(this BinaryReader reader) { var header = new Header { - size = reader.ReadInt32(), - hash = reader.ReadString(), - name = reader.ReadString(), - guid = reader.ReadGuid(), - ivLength = reader.ReadInt32(), + Hash = reader.ReadString(), + IsFolder = reader.ReadBoolean(), + Name = reader.ReadString(), + Guid = reader.ReadGuid(), + IvLength = reader.ReadInt32(), }; - header.iv = reader.ReadBytes(header.ivLength); + header.Iv = reader.ReadBytes(header.IvLength); return header; } #endregion - - #region safeFile - private static readonly string _currentPath = Environment.CurrentDirectory; - private static readonly string _safeFilePath = $"{_currentPath}/.safe"; - - public static bool GetStateFromSafeFile() - { - using var binaryReader = new BinaryReader(File.OpenRead(_safeFilePath)); - var state = binaryReader.ReadBoolean(); - return state; - } - public static void SetStateToSafeFile(bool state) + #region Cryptography + public static string HashBytes(byte[] bytes) { - using var binaryWriter = new BinaryWriter(File.OpenWrite(_safeFilePath)); - binaryWriter.Write(state); - } - - public static string GetHashFromSafeFile() - { - try { - using var binaryReader = new BinaryReader(File.OpenRead(_safeFilePath)); - _ = binaryReader.ReadBoolean(); - var hash = binaryReader.ReadString(); - return hash; - } catch (Exception) { - return ""; - } + using var sha = SHA512.Create(); + return Convert.ToHexString(sha.ComputeHash(bytes)); } - public static IEnumerable GetFilesFromSafeFile() + /// + /// Creates a key based on one or two strings. String -> Byte[] uses UTF8 + /// + /// The main input + /// The salt used. If , the salt will be a empty array + /// The Key derived + public static byte[] DeriveKeyFromString(string input, string? salt = null) { - using var binaryReader = new BinaryReader(File.OpenRead(_safeFilePath)); - _ = binaryReader.ReadBoolean(); - _ = binaryReader.ReadString(); - var files = binaryReader.ReadStrings(); - return files; + //get input bytes + byte[] inputBytes = Encoding.UTF8.GetBytes(input); + byte[] saltBytes = salt != null ? Encoding.UTF8.GetBytes(salt) : new byte[16]; + // Generate the hash + Rfc2898DeriveBytes pbkdf2 = new(inputBytes, saltBytes, iterations: 5000, HashAlgorithmName.SHA512); + return pbkdf2.GetBytes(32); //32 bytes length is 256 bits } - public static void SetFilesToSafeFile() - { - var hash = GetHashFromSafeFile(); - var iv = Utils.GenerateIv(); - - using var binaryWriter = new BinaryWriter(File.OpenWrite(_safeFilePath)); - binaryWriter.Write(false); - binaryWriter.Write(hash); - binaryWriter.Write(Installer.GetFiles()); - binaryWriter.Write(Installer.GetFolders()); - binaryWriter.Write(iv.Length); - binaryWriter.Write(iv); - } - - public static IEnumerable GetFoldersFromSafeFile() + /// + /// Generates a random iv for AES + /// + /// The IV that has been generated + public static byte[] GenerateIv() { - using var binaryReader = new BinaryReader(File.OpenRead(_safeFilePath)); - _ = binaryReader.ReadBoolean(); - _ = binaryReader.ReadString(); - _ = binaryReader.ReadStrings(); - var folders = binaryReader.ReadStrings(); - return folders; + //generate random IV + using var aes = Aes.Create(); + return aes.IV; } - - public static byte[] GetIvFromSafeFile() - { - using var binaryReader = new BinaryReader(File.OpenRead(_safeFilePath)); - _ = binaryReader.ReadBoolean(); - _ = binaryReader.ReadString(); - _ = binaryReader.ReadStrings(); - _ = binaryReader.ReadStrings(); - var length = binaryReader.ReadInt32(); - var iv = binaryReader.ReadBytes(length); - return iv; + public static string GetHash(string str){ + return BCrypt.Net.BCrypt.HashPassword(str); + } + public static bool CheckHash(string password, string hash){ + return BCrypt.Net.BCrypt.Verify(password, hash); } - #endregion - - #region Cryptography + /// + /// Deletes a file in a secure way by overwriting it with + /// random garbage data n times. + /// + /// The progessbar object, null if it's on a cli run + /// Full path of the file to be deleted + public static void WipeFile(string filename, Progress? prog) { + bool verbose = prog is not null; + try + { + if (!File.Exists(filename)) return; + // Set the files attributes to normal in case it's read-only. + File.SetAttributes(filename, FileAttributes.Normal); - public static string HashFile(string path){ - //hash file - using var fs = File.OpenRead(path); - using var sha = SHA256.Create(); - var hashBytes = sha.ComputeHash(fs); - var hash = Convert.ToBase64String(hashBytes); + // Calculate the total number of sectors in the file. + var sectors = (int)Math.Ceiling(new FileInfo(filename).Length/512.0); + + // Create a dummy-buffer the size of a sector. + var buffer = new byte[512]; - return hash; + // Open a FileStream to the file. + var inputStream = new FileStream(filename, FileMode.Open); + + // Loop all sectors + for (var i = 0; i < sectors; i++) + { + // write zeros + inputStream.Write(buffer, 0, buffer.Length); + } + // truncate file + inputStream.SetLength(0); + // Close the stream. + inputStream.Close(); + + // wipe dates + var dt = new DateTime(2037, 1, 1, 0, 0, 0); + File.SetCreationTime(filename, dt); + File.SetLastAccessTime(filename, dt); + File.SetLastWriteTime(filename, dt); + + File.SetCreationTimeUtc(filename, dt); + File.SetLastAccessTimeUtc(filename, dt); + File.SetLastWriteTimeUtc(filename, dt); + + // Finally, delete the file + File.Delete(filename); + + prog?.Message(Message.LEVEL.DEBUG, $"{Path.GetFileName(filename)} cleared traces successfully"); + } + catch(Exception e) { + if (verbose) + prog?.Message(Message.LEVEL.ERROR, $"Error wiping file ({Path.GetFileName(filename)})" + e.Message); + else + WriteLine($"Error wiping file ({Path.GetFileName(filename)}): {e.Message}", ConsoleColor.Red); + } } - - public static byte[] CreateKey(string hash, string password) => Convert.FromHexString(RawHash(hash + password)); - public static byte[] GenerateIv() + public static void WipeFolder(string folder, Progress? prog) { - //generate random IV - using var aes = Aes.Create(); - return aes.IV; - } + try + { + if (!Directory.Exists(folder)) + return; - public static string GetHash(string str){ - return BCrypt.Net.BCrypt.HashPassword(str); - } - private static string RawHash(string s){ - //sha256 - var sha256 = SHA256.Create(); - var bytes = System.Text.Encoding.UTF8.GetBytes(s); - var hash = sha256.ComputeHash(bytes); - return BitConverter.ToString(hash).Replace("-", "").ToLower(); - } + DirectoryInfo dir = new(folder); + FileInfo[] files = dir.GetFiles(); + DirectoryInfo[] dirs = dir.GetDirectories(); - #endregion + foreach (FileInfo file in files) + { + WipeFile(file.FullName, prog); + } + + foreach (DirectoryInfo subDir in dirs) + { + WipeFolder(subDir.FullName, prog); + } + + // wipe dates + DateTime dt = new DateTime(2037, 1, 1, 0, 0, 0); + Directory.SetCreationTime(folder, dt); + Directory.SetLastAccessTime(folder, dt); + Directory.SetLastWriteTime(folder, dt); + + Directory.SetCreationTimeUtc(folder, dt); + Directory.SetLastAccessTimeUtc(folder, dt); + Directory.SetLastWriteTimeUtc(folder, dt); + + // Finally, delete the folder + Directory.Delete(folder, true); + + prog?.Message(Message.LEVEL.DEBUG, $"{Path.GetFileName(folder)} cleared traces successfully"); + } + catch(Exception e) + { + prog?.Message(Message.LEVEL.ERROR, $"Error wiping folder ({Path.GetFileName(folder)})" + e.Message); + } + } + #endregion } }