You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

727 lines
30 KiB

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 <password>] [<key file>] [-e|-d] <input file> [<output file>]");
Console.WriteLine("encaes [-p|-P <password>] --keygen <key file>");
Console.WriteLine("encaes [-p|-P <password>] --regenkey <key file> <new key file>");
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<string> 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<string>(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<AESKey>();
}
}
}
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<FileHeader>(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<FileHeader>(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<string> ReadPassword(string prompt = null, CancellationToken token = default)
{
TaskCompletionSource<string> comp = new TaskCompletionSource<string>();
List<char> pwd = new List<char>();
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<string> getPasswordFunc)
{
var header = from.ReadValueUnmanaged<KeyHeader>();
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<AESKey>();
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<AESKey>();
}
}
}
}
finally
{
aesk.ZeroMemory();
}
}
}
else
{
return from.ReadValueUnmanaged<AESKey>();
}
}
static unsafe void GenerateKey(Stream to, Options opt)
{
Span<byte> 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<AESKey>();
}
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<AESKey>();
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<AESKey>();
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<AESKey>();
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<byte> 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<T>
{
private readonly object mutex = new object();
private readonly IEnumerator<T> iter;
public Taker(IEnumerable<T> 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;
}
}
}