commit 81d9fa61b6fce786e4b7992e762c514b2382ad5d Author: Avril Date: Thu Apr 23 18:46:22 2020 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83ba081 --- /dev/null +++ b/.gitignore @@ -0,0 +1,360 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*[.json, .xml, .info] + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd diff --git a/encaes.sln b/encaes.sln new file mode 100644 index 0000000..2c32ed1 --- /dev/null +++ b/encaes.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29609.76 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "encaes", "encaes\encaes.csproj", "{5B2296BB-5E15-4DA4-BE4F-2E5EDE3999AE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5B2296BB-5E15-4DA4-BE4F-2E5EDE3999AE}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {5B2296BB-5E15-4DA4-BE4F-2E5EDE3999AE}.Debug|Any CPU.Build.0 = Release|Any CPU + {5B2296BB-5E15-4DA4-BE4F-2E5EDE3999AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B2296BB-5E15-4DA4-BE4F-2E5EDE3999AE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7AF88A03-C952-46EF-BABC-CD4A918D82AF} + EndGlobalSection +EndGlobal diff --git a/encaes/Api.cs b/encaes/Api.cs new file mode 100644 index 0000000..a2619ba --- /dev/null +++ b/encaes/Api.cs @@ -0,0 +1,408 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using System.Threading; +using System.Threading.Channels; + +using Tools; +using Tools.Crypto; +using System.Security.Cryptography; + +namespace encaes +{ + public sealed class AsyncMutex : IDisposable + { + private class DisposeHook : IDisposable + { + public event Action OnDispose; + public DisposeHook(Action ondispose) + { + OnDispose = ondispose; + } + public DisposeHook() { } + public void Dispose() + => OnDispose?.Invoke(); + } + private readonly SemaphoreSlim sem; + public AsyncMutex() + { + sem = new SemaphoreSlim(1, 1); + } + + private AsyncMutex(SemaphoreSlim sem) + { + this.sem = sem; + } + + public async Task AquireAsync(CancellationToken token = default) + { + await sem.WaitAsync(token); + return new DisposeHook(() => sem.Release()); + } + + public IDisposable Aquire() + { + sem.Wait(); + return new DisposeHook(() => sem.Release()); + } + + public void Dispose() + { + sem.Dispose(); + } + + public static AsyncMutex Semaphore(int count, int max) + { + return new AsyncMutex(new SemaphoreSlim(count, max)); + } + public static AsyncMutex Semaphore(int count) + { + return new AsyncMutex(new SemaphoreSlim(count, count)); + } + } + public class AesEncryptor : IDisposable + { + public static Task ReadPassword(string prompt = null, CancellationToken token = default) + { + TaskCompletionSource comp = new TaskCompletionSource(); + List pwd = new List(); + + bool set = false; + _ = Task.Run(() => + { + if (prompt != null) + Console.Write(prompt); + try + { + using var _c = token.Register(() => + { + if (!set) + { + set = true; + comp.SetException(new OperationCanceledException()); + } + }); + while (true) + { + ConsoleKeyInfo i = Console.ReadKey(true); + if (token.IsCancellationRequested) break; + if (i.Key == ConsoleKey.Enter) + { + Console.WriteLine(); + break; + } + else if (i.Key == ConsoleKey.Backspace) + { + if (pwd.Count > 0) + { + pwd.RemoveAt(pwd.Count - 1); + } + } + else if (i.KeyChar != '\u0000') + { + pwd.Add(i.KeyChar); + } + } + + if (!set) + { + set = true; + if (token.IsCancellationRequested) + comp.SetException(new OperationCanceledException()); + else + comp.SetResult(new string(pwd.ToArray())); + } + } + catch (Exception ex) + { + Console.WriteLine(); + pwd.Clear(); + if (!set) + { + set = true; + comp.SetException(ex); + } + } + }); + return comp.Task; + } + + protected readonly AsyncMutex readmutex = new AsyncMutex(); + protected readonly CancellationTokenSource cancel = new CancellationTokenSource(); + public Stream File { get; } + + public bool KeepAlive { get; set; } = false; + + public void CancelAllOperations() + { + if (!cancel.IsCancellationRequested) + cancel.Cancel(); + } + + public AesEncryptor(Stream file) + { + File = file; + } + + /// + /// Check if is encrypted. + /// + public async Task StatAsync(CancellationToken token = default) + { + using var cancel = CancellationTokenSource.CreateLinkedTokenSource(this.cancel.Token, token); + + using (await readmutex.AquireAsync(cancel.Token)) + { + if (File.Length < FileHeader.Size) return false; + + File.Position = 0; + return (await File.ReadValueUnmanagedAsync(cancel.Token)).Valid; + } + } + + public static unsafe AESKey GenerateKeyFromPassword(string password) + { + using (var derive = new Rfc2898DeriveBytes(password, global::encaes.Properties.Resources.globalSalt)) + { + return derive.GetBytes(sizeof(AESKey)).ToUnmanaged(); + } + } + public static async Task LoadKey(Stream from, Func getPassword, CancellationToken token = default) + { + return await LoadKey(from, async () => + { + await Task.Yield(); + return getPassword(); + }, token); + } + public static async Task LoadKey(Stream from, Func> getPassword, CancellationToken token = default) + { + if (from.Length < KeyHeader.Size) + { + throw new InvalidDataException("Not a key: Too small."); + } + var kh = await from.ReadValueUnmanagedAsync(token); + if (kh.Check != KeyHeader.CheckBit) + { + throw new InvalidDataException("Not a key: Bad header."); + } + if (kh.PasswordProtected) + { + var passwd = await getPassword(); + var phash = KeyHeader.HashAndSalt(passwd, kh.Salt.ToByteArrayUnmanaged()); + if (phash != kh.PasswordHash) + throw new InvalidDataException("Bad password"); + using (var derive = new Rfc2898DeriveBytes(passwd, kh.Salt.ToByteArrayUnmanaged())) + { + AESKey pkey; + unsafe + { + pkey = derive.GetBytes(sizeof(AESKey)).ToUnmanaged(); + } + using (var aes = Aes.Create()) + { + pkey.ToAes(aes); + using (var dec = aes.CreateDecryptor()) + { + using (var cs = new CryptoStream(from, dec, CryptoStreamMode.Read)) + return await cs.ReadValueUnmanagedAsync(token); + } + } + } + } + else + return await from.ReadValueUnmanagedAsync(token); + } + public static Task GenerateKey(Stream to, CancellationToken token = default) => GenerateKey(to, null, token); + public static async Task GenerateKey(Stream to, string passwordProtect, CancellationToken token = default) + { + int aesksize; + unsafe + { + aesksize = sizeof(AESKey); + } + if (passwordProtect != null) + { + byte[] salt = new byte[Program.PasswordSaltSize]; + byte[] key = new byte[aesksize]; + + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(key); + rng.GetBytes(salt); + } + + var passwordHash = KeyHeader.HashAndSalt(passwordProtect, salt); + + KeyHeader kh = KeyHeader.Create(passwordHash, new Tools.ByValData.Fixed.FixedByteArray16(salt)); + await to.WriteValueUnmanagedAsync(kh, token); + + using (var derive = new Rfc2898DeriveBytes(passwordProtect, salt)) + { + AESKey pkey; + unsafe + { + pkey = derive.GetBytes(sizeof(AESKey)).ToUnmanaged(); + } + using (var aes = Aes.Create()) + { + pkey.ToAes(aes); + using (var dec = aes.CreateEncryptor()) + { + using (var cs = new CryptoStream(to, dec, CryptoStreamMode.Write)) + await cs.WriteAllAsync(key, token); + } + } + } + + return key.ToUnmanaged(); + } + else + { + AESKey k; + await to.WriteValueUnmanagedAsync(KeyHeader.Create(), token); + await to.WriteValueUnmanagedAsync(k = AESKey.NewKey(), token); + return k; + } + } + + public AESKey Key { get; set; } = AESKey.NewKey(); + + public static Task SaveKey(Stream to, AESKey key, CancellationToken token = default) + => SaveKey(to, key, null, token); + public static async Task SaveKey(Stream to, AESKey key, string passwordProtect, CancellationToken token = default) + { + if (passwordProtect == null) + { + await to.WriteValueUnmanagedAsync(KeyHeader.Create(), token); + await to.WriteValueUnmanagedAsync(key, token); + } + else + { + var salt = new byte[Program.PasswordSaltSize]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(salt); + } + var pwhash = KeyHeader.HashAndSalt(passwordProtect, salt); + var kh = KeyHeader.Create(pwhash, new Tools.ByValData.Fixed.FixedByteArray16(salt)); + + await to.WriteValueUnmanagedAsync(kh, token); + using (var derive = new Rfc2898DeriveBytes(passwordProtect, salt)) + { + int ak; + unsafe + { + ak = sizeof(AESKey); + } + AESKey pkey = derive.GetBytes(ak).ToUnmanaged(); + + using (var aes = Aes.Create()) + { + pkey.ToAes(aes); + using (var enc = aes.CreateEncryptor()) + { + using (var cs = new CryptoStream(to, enc, CryptoStreamMode.Write)) + await cs.WriteValueUnmanagedAsync(key, token); + } + } + } + } + } + + public async Task Encrypt(Stream to, CancellationToken token=default) + { + using var cancel = CancellationTokenSource.CreateLinkedTokenSource(this.cancel.Token, token); + using var aes = Aes.Create(); + Key.ToAes(aes); + + using (await readmutex.AquireAsync(cancel.Token)) + { + File.Position = 0; + + var header = FileHeader.Create(); + await to.WriteValueUnmanagedAsync(header, cancel.Token); + + using (var enc = aes.CreateEncryptor()) + { + var cs = new CryptoStream(to, enc, CryptoStreamMode.Write); + await File.CopyToAsync(cs, cancel.Token); + cs.FlushFinalBlock(); + await cs.FlushAsync(); + } + } + } + + public async Task Decrypt(Stream to, CancellationToken token = default) + { + using var cancel = CancellationTokenSource.CreateLinkedTokenSource(this.cancel.Token, token); + using var aes = Aes.Create(); + Key.ToAes(aes); + + using (await readmutex.AquireAsync(cancel.Token)) + { + File.Position = 0; + + var header = await File.ReadValueUnmanagedAsync(cancel.Token); + + if (!header.Valid) throw new InvalidDataException("Not an encrypted stream."); + + using (var dec = aes.CreateDecryptor()) + { + using (var cs = new CryptoStream(File, dec, CryptoStreamMode.Read)) + await cs.CopyToAsync(to, cancel.Token); + } + } + } + + public static async Task IsEncryptedFile(Stream stream, CancellationToken token=default) + { + if (stream.Length < FileHeader.Size) return false; + var fh = await stream.ReadValueUnmanagedAsync(token); + return fh.Valid; + } + + public static async Task IsEncryptedFile(string filename, CancellationToken token = default) + { + await using (var fs = new FileStream(filename, FileMode.Open, FileAccess.Read)) + return await IsEncryptedFile(fs, token); + } + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + CancelAllOperations(); + readmutex.Dispose(); + cancel.Dispose(); + + if (!KeepAlive) + File.Dispose(); + } + + Key = default; + + disposedValue = true; + } + } + + ~AesEncryptor() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + #endregion + } +} diff --git a/encaes/Program.cs b/encaes/Program.cs new file mode 100644 index 0000000..030fd3a --- /dev/null +++ b/encaes/Program.cs @@ -0,0 +1,726 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using Tools; +using Tools.Crypto; +using System.Linq; +using System.Threading.Tasks; +using System.Threading; + +namespace encaes +{ + enum Mode + { + Failed=0, + Auto, + Encrypt, + Decrypt, + Genkey, + RegenKey, + } + struct Options + { + public bool SubstitutePassword; + public string Password; + public string KeyFile; + public string NewKeyFile; + public string InputFile; + public string OutputFile; + public bool InPlace; + + } + class Program + { + static void usage() + { + Console.WriteLine("encaes [-p|-P ] [] [-e|-d] []"); + Console.WriteLine("encaes [-p|-P ] --keygen "); + Console.WriteLine("encaes [-p|-P ] --regenkey "); + + Console.WriteLine("\n-p\tSpecify password for key."); + Console.WriteLine("-P\tSpecify password instead of key."); + Console.WriteLine("-e\tForce encrypt."); + Console.WriteLine("-e\tForce decrypt."); + Console.WriteLine("--keygen\tGenerate key"); + Console.WriteLine("--regenkey\tRegenerate key"); + } + static (Mode Mode, Options Options) ParseArgs(Taker args) + { + Mode mode = Mode.Failed; + Options opt = default; + try + { + + while (args.TryTake(out string arg0)) + { + if (arg0 == "-p" && opt.Password == null) + { + opt.Password = args.Take(); + continue; + } + else if (arg0 == "-P" && opt.Password == null) + { + opt.Password = args.Take(); + opt.SubstitutePassword = true; + continue; + } + else if (arg0 == "--keygen" && args.TryTake(out opt.KeyFile)) + { + mode = Mode.Genkey; + return (mode, opt); + } + else if(arg0 == "--regenkey" && args.TryTake(out opt.KeyFile)) + { + mode = Mode.RegenKey; + opt.NewKeyFile = args.Take(); + return (mode, opt); + } + else if (!opt.SubstitutePassword && opt.KeyFile==null) + opt.KeyFile = arg0; + else + { + if (mode == Mode.Failed) + { + string encmode = arg0; + switch (encmode) + { + case "-e": + mode = Mode.Encrypt; + break; + case "-d": + mode = Mode.Decrypt; + break; + default: + mode = Mode.Auto; + opt.InputFile = encmode; + opt.InPlace = !args.TryTake(out opt.OutputFile); + return (mode, opt); + } + opt.InputFile = args.Take(); + opt.InPlace = !args.TryTake(out opt.OutputFile); + return (mode, opt); + } + } + } + } + catch(IndexOutOfRangeException) + { + return (Mode.Failed, default); + } + return (mode, opt); + } + static async Task Main(string[] args) + { + if (args.Length < 2) + { + usage(); + return; + } + var (mode, opt) = ParseArgs(new Taker(args)); + + AESKey getKey() + { + try + { + if (opt.SubstitutePassword) + { + using (var derive = new Rfc2898DeriveBytes(opt.Password, global::encaes.Properties.Resources.globalSalt)) + { + unsafe + { + return derive.GetBytes(sizeof(AESKey)).ToUnmanaged(); + } + } + } + else + { + using (var stream = new FileStream(opt.KeyFile, FileMode.Open, FileAccess.Read)) + { + return ReadAesKey(stream, () => + { + if (opt.Password == null) throw new InvalidOperationException("Key is password protected"); + else + return opt.Password; + }); + } + } + }catch(Exception ex) + { + throw new InvalidOperationException("Key loading failed: " + ex.Message); + } + } + + switch (mode) + { + case Mode.Genkey: + try + { + using (var kf = new FileStream(opt.KeyFile, FileMode.Create)) + { + GenerateKey(kf, opt); + } + } + catch (Exception ex) + { + Console.WriteLine("Error: Keygen failed (" + ex.GetType().Name + "): " + ex.Message); + } + break; + case Mode.Encrypt: + try + { + if (opt.InPlace) throw new NotImplementedException("In-place encryption not yet supported."); + using (var aes = Aes.Create()) + { + using var cancel = new CancellationTokenSource(); + + void kill(object sender, ConsoleCancelEventArgs ev) + { + if (!cancel.IsCancellationRequested) + { + cancel.Cancel(); + ev.Cancel = true; + } + else Console.WriteLine("Force exit"); + } + + Console.CancelKeyPress += kill; + try + { + var key = getKey(); + try + { + key.ToAes(aes); + using (var enc = aes.CreateEncryptor()) + { + await using (var readStream = new FileStream(opt.InputFile, FileMode.Open, FileAccess.Read)) + { + await using (var outputStream = new FileStream(opt.OutputFile, FileMode.Create)) + { + var fh = FileHeader.Create(); + await outputStream.WriteValueUnmanagedAsync(fh, cancel.Token); + using (var cs = new CryptoStream(outputStream, enc, CryptoStreamMode.Write)) + { + await readStream.CopyToAsync(cs, cancel.Token); + } + } + } + } + } + finally { key.ZeroMemory(); } + } + finally + { + Console.CancelKeyPress -= kill; + } + } + }catch(Exception ex) + { + Console.WriteLine("Error: Encryption failed ("+ex.GetType().Name+"): " + ex.Message); + } + break; + case Mode.Decrypt: + try + { + if (opt.InPlace) throw new NotImplementedException("In-place encryption not yet supported."); + using (var aes = Aes.Create()) + { + using var cancel = new CancellationTokenSource(); + + void kill(object sender, ConsoleCancelEventArgs ev) + { + if (!cancel.IsCancellationRequested) + { + cancel.Cancel(); + ev.Cancel = true; + } + else Console.WriteLine("Force exit"); + } + + Console.CancelKeyPress += kill; + try + { + var k = getKey(); + try + { + k.ToAes(aes); + + await using (var inputFile = new FileStream(opt.InputFile, FileMode.Open, FileAccess.Read)) + { + var fh = await inputFile.ReadValueUnmanagedAsync(cancel.Token); + if (!fh.Valid) throw new InvalidOperationException("Not an encrypted file"); + + using (var dec = aes.CreateDecryptor()) + { + await using (var outputFile = new FileStream(opt.OutputFile, FileMode.Create)) + { + using (var cs = new CryptoStream(inputFile, dec, CryptoStreamMode.Read)) + { + await cs.CopyToAsync(outputFile, cancel.Token); + } + } + } + } + } + finally { k.ZeroMemory(); } + } + finally + { + Console.CancelKeyPress -= kill; + } + } + } + catch (Exception ex) + { + Console.WriteLine("Error: Decryption failed (" + ex.GetType().Name + "): " + ex.Message); + } + break; + case Mode.Auto: + try + { + if (opt.InPlace) throw new NotImplementedException("In-place encryption not yet supported."); + using (var aes = Aes.Create()) + { + using var cancel = new CancellationTokenSource(); + + void kill(object sender, ConsoleCancelEventArgs ev) + { + if (!cancel.IsCancellationRequested) + { + cancel.Cancel(); + ev.Cancel = true; + } + else Console.WriteLine("Force exit"); + } + + Console.CancelKeyPress += kill; + try + { + var k = getKey(); + //Console.WriteLine("Using key " + BitConverter.ToString(k.ToByteArrayUnmanaged().SHA256Hash()).Replace("-", "").ToLower()); + try + { + k.ToAes(aes); + await using (var inputFile = new FileStream(opt.InputFile, FileMode.Open, FileAccess.Read)) + { + FileHeader readh; + if (inputFile.Length < FileHeader.Size || !((readh = await inputFile.ReadValueUnmanagedAsync(cancel.Token))).Valid) + { + inputFile.Position = 0; + //Encrypting. + using (var enc = aes.CreateEncryptor()) + { + await using (var outputFile = new FileStream(opt.OutputFile, FileMode.Create)) + { + var fh = FileHeader.Create(); + await outputFile.WriteValueUnmanagedAsync(fh, cancel.Token); + using (var cs = new CryptoStream(outputFile, enc, CryptoStreamMode.Write)) + { + await inputFile.CopyToAsync(cs, cancel.Token); + cs.Flush(); + if (!cs.HasFlushedFinalBlock) + cs.FlushFinalBlock(); + } + } + } + } + else + { + //Decrypting. + using (var dec = aes.CreateDecryptor()) + { + await using (var outputFile = new FileStream(opt.OutputFile, FileMode.Create)) + { + using (var cs = new CryptoStream(inputFile, dec, CryptoStreamMode.Read)) + { + await cs.CopyToAsync(outputFile, cancel.Token); + cs.Flush(); + if (!cs.HasFlushedFinalBlock) + cs.FlushFinalBlock(); + } + } + } + } + } + } + finally { k.ZeroMemory(); } + } + finally + { + Console.CancelKeyPress -= kill; + } + } + } + catch (Exception ex) + { + Console.WriteLine("Error: Auto failed (" + ex.GetType().Name + "): " + ex.Message); + } + break; + case Mode.RegenKey: + try + { + using var cancel = new CancellationTokenSource(); + + void kill(object sender, ConsoleCancelEventArgs ev) + { + if (!cancel.IsCancellationRequested) + { + cancel.Cancel(); + ev.Cancel = true; + } + else Console.WriteLine("Force exit"); + } + + Console.CancelKeyPress += kill; + try + { + AESKey key; + using (var kfs = new FileStream(opt.KeyFile, FileMode.Open, FileAccess.Read)) + { + key = await AesEncryptor.LoadKey(kfs, async () => + { + return await ReadPassword("Key Password: ", cancel.Token); + }, cancel.Token); + } + using (var ofs = new FileStream(opt.NewKeyFile, FileMode.Create)) + { + await AesEncryptor.SaveKey(ofs, key, opt.Password, cancel.Token); + } + } + finally + { + Console.CancelKeyPress -= kill; + } + } + catch (Exception ex) + { + Console.WriteLine("Error: Regen failed (" + ex.GetType().Name + "): " + ex.Message); + } + + break; + case Mode.Failed: + default: + usage(); + return; + } + } + + static Task ReadPassword(string prompt = null, CancellationToken token = default) + { + TaskCompletionSource comp = new TaskCompletionSource(); + List pwd = new List(); + + bool set = false; + _ = Task.Run(() => + { + if (prompt != null) + Console.Write(prompt); + try + { + using var _c = token.Register(() => + { + if (!set) + { + set = true; + comp.SetException(new OperationCanceledException()); + } + }); + while (true) + { + ConsoleKeyInfo i = Console.ReadKey(true); + if (token.IsCancellationRequested) break; + if (i.Key == ConsoleKey.Enter) + { + Console.WriteLine(); + break; + } + else if (i.Key == ConsoleKey.Backspace) + { + if (pwd.Count > 0) + { + pwd.RemoveAt(pwd.Count - 1); + } + } + else if (i.KeyChar != '\u0000') + { + pwd.Add(i.KeyChar); + } + } + + if (!set) + { + set = true; + if (token.IsCancellationRequested) + comp.SetException(new OperationCanceledException()); + else + comp.SetResult(new string(pwd.ToArray())); + } + } + catch (Exception ex) + { + Console.WriteLine(); + pwd.Clear(); + if (!set) + { + set = true; + comp.SetException(ex); + } + } + }); + return comp.Task; + } + static unsafe AESKey ReadAesKey(Stream from, Func getPasswordFunc) + { + var header = from.ReadValueUnmanaged(); + if (header.Check != KeyHeader.CheckBit) throw new InvalidDataException("This is not a key"); + if (header.PasswordProtected) + { + var passw = getPasswordFunc(); + if (header.PasswordHash != KeyHeader.HashAndSalt(passw, header.Salt.ToByteArrayUnmanaged())) + throw new InvalidDataException("Bad password hash"); + using (var derive = new Rfc2898DeriveBytes(passw, header.Salt)) + { + var aesk = derive.GetBytes(sizeof(AESKey)).ToUnmanaged(); + try + { + using (var aes = Aes.Create()) + { + aesk.ToAes(aes); + using (var dec = aes.CreateDecryptor()) + { + using (var cs = new CryptoStream(from, dec, CryptoStreamMode.Read)) + { + return cs.ReadValueUnmanaged(); + } + } + } + } + finally + { + aesk.ZeroMemory(); + } + } + } + else + { + return from.ReadValueUnmanaged(); + } + } + + static unsafe void GenerateKey(Stream to, Options opt) + { + Span salt = stackalloc byte[PasswordSaltSize]; + using var rng = RandomNumberGenerator.Create(); + if (opt.Password != null) + { + rng.GetBytes(salt); + } + + KeyHeader header; + AESKey key; + try + { + if (opt.SubstitutePassword) + { + using (var derive = new Rfc2898DeriveBytes(opt.Password, salt.ToArray())) + { + key = derive.GetBytes(sizeof(AESKey)).ToUnmanaged(); + } + header = KeyHeader.Create(); + + to.WriteValueUnmanaged(header); + to.WriteValueUnmanaged(key); + } + else if (opt.Password != null) + { + key = AESKey.NewKey(); + header = KeyHeader.Create(KeyHeader.HashAndSalt(opt.Password, salt), new Tools.ByValData.Fixed.FixedByteArray16(salt.ToArray())); + + to.WriteValueUnmanaged(header); + using (var derive = new Rfc2898DeriveBytes(opt.Password, salt.ToArray())) + { + AESKey pwk = derive.GetBytes(sizeof(AESKey)).ToUnmanaged(); + using (var aes = Aes.Create()) + { + pwk.ToAes(aes); + using (var enc = aes.CreateEncryptor()) + { + using (var cs = new CryptoStream(to, enc, CryptoStreamMode.Write)) + cs.WriteValueUnmanaged(key); + } + } + } + } + else + { + key = AESKey.NewKey(); + header = KeyHeader.Create(); + + to.WriteValueUnmanaged(header); + to.WriteValueUnmanaged(key); + } + } + finally + { + key.ZeroMemory(); + } + } + + static unsafe byte[] PasswordDecrypt(byte[] buffer, string password, byte[] salt) + { + using (var derive = new Rfc2898DeriveBytes(password, salt)) + { + var aesk = derive.GetBytes(sizeof(AESKey)).ToUnmanaged(); + try + { + using (var aes = Aes.Create()) + { + aesk.ToAes(aes); + using (var dec = aes.CreateDecryptor()) + { + using (var ms = new MemoryStream(buffer)) + { + ms.Position = 0; + using (var cs = new CryptoStream(ms, dec, CryptoStreamMode.Read)) + { + using (var oms = new MemoryStream()) + { + cs.CopyTo(oms); + return oms.ToArray(); + } + } + } + } + } + } + finally + { + aesk.ZeroMemory(); + } + } + } + static unsafe byte[] PasswordEncrypt(byte[] buffer, string password, byte[] salt) + { + using (var derive = new Rfc2898DeriveBytes(password, salt)) + { + var aesk = derive.GetBytes(sizeof(AESKey)).ToUnmanaged(); + try + { + using (var aes = Aes.Create()) + { + aesk.ToAes(aes); + using (var enc = aes.CreateEncryptor()) + { + using (var ms = new MemoryStream()) + { + using (var cs = new CryptoStream(ms, enc, CryptoStreamMode.Write)) + { + cs.Write(buffer, 0, buffer.Length); + cs.Flush(); + } + return ms.ToArray(); + } + } + } + } + finally + { + aesk.ZeroMemory(); + } + } + } + + public const int PasswordSaltSize= 16; + } + + public unsafe readonly struct FileHeader + { + public static readonly int Size = sizeof(FileHeader); + + public const uint CheckBit = 0x2BAD1DEA; + + public readonly uint Check; + //TODO: Add hash + public readonly SHA256Hash FileHash;//TODO; + + private FileHeader(uint chk) + { + Check = chk; + } + + public static FileHeader Create() + { + return new FileHeader(CheckBit); + } + + public bool Valid => Check == CheckBit; + } + + public unsafe readonly struct KeyHeader + { + public static readonly int Size = sizeof(KeyHeader); + public const uint CheckBit = 0xABAD1DEA; + + public readonly uint Check; + public readonly bool PasswordProtected; + public readonly Tools.ByValData.Fixed.FixedByteArray16 Salt; + public readonly SHA256Hash PasswordHash; + + public static SHA256Hash HashAndSalt(string password, Span salt) + { + return Encoding.UTF8.GetBytes(password).Concat(salt.ToArray()).ToArray().SHA256Hash(); + } + + private KeyHeader(uint chk, bool pwd, Tools.ByValData.Fixed.FixedByteArray16 salt, SHA256Hash hash) + { + Check = chk; + PasswordProtected = pwd; + Salt = salt; + PasswordHash = hash; + } + + public static KeyHeader Create(SHA256Hash? PasswordHash=null, Tools.ByValData.Fixed.FixedByteArray16 PasswordSalt=default) + { + if (PasswordHash == null) + { + return new KeyHeader(CheckBit, false, default, default); + } + else + return new KeyHeader(CheckBit, true, PasswordSalt, PasswordHash.Value); + } + } + + sealed class Taker + { + private readonly object mutex = new object(); + private readonly IEnumerator iter; + public Taker(IEnumerable from) + { + iter = from.GetEnumerator(); + } + + public bool TryTake(out T value) + { + lock (mutex) + { + if (iter.MoveNext()) + { + value = iter.Current; + return true; + } + } + value = default; + return false; + } + + public T Take() + { + if (!TryTake(out T value)) + throw new IndexOutOfRangeException(); + else + return value; + } + } + +} diff --git a/encaes/Properties/Resources.Designer.cs b/encaes/Properties/Resources.Designer.cs new file mode 100644 index 0000000..e8ec038 --- /dev/null +++ b/encaes/Properties/Resources.Designer.cs @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace encaes.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("encaes.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] globalSalt { + get { + object obj = ResourceManager.GetObject("globalSalt", resourceCulture); + return ((byte[])(obj)); + } + } + } +} diff --git a/encaes/Properties/Resources.resx b/encaes/Properties/Resources.resx new file mode 100644 index 0000000..bd91099 --- /dev/null +++ b/encaes/Properties/Resources.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\globalSalt;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/encaes/encaes.csproj b/encaes/encaes.csproj new file mode 100644 index 0000000..fdb8187 --- /dev/null +++ b/encaes/encaes.csproj @@ -0,0 +1,43 @@ + + + + Exe + netcoreapp3.1 + + + + true + + + + true + + + + + ..\..\extensions\excryptotools\bin\Release\netstandard2.0\exbintools.dll + + + ..\..\extensions\excryptotools\bin\Release\netstandard2.0\excryptotools.dll + + + ..\..\extensions\exstreamtools\bin\Release\netstandard2.0\exstreamtools.dll + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/encaes/globalSalt b/encaes/globalSalt new file mode 100644 index 0000000..6c393c2 Binary files /dev/null and b/encaes/globalSalt differ