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.
409 lines
14 KiB
409 lines
14 KiB
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<IDisposable> 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<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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if <see cref="File"/> is encrypted.
|
|
/// </summary>
|
|
public async Task<bool> 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<FileHeader>(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<AESKey>();
|
|
}
|
|
}
|
|
public static async Task<AESKey> LoadKey(Stream from, Func<string> getPassword, CancellationToken token = default)
|
|
{
|
|
return await LoadKey(from, async () =>
|
|
{
|
|
await Task.Yield();
|
|
return getPassword();
|
|
}, token);
|
|
}
|
|
public static async Task<AESKey> LoadKey(Stream from, Func<Task<string>> getPassword, CancellationToken token = default)
|
|
{
|
|
if (from.Length < KeyHeader.Size)
|
|
{
|
|
throw new InvalidDataException("Not a key: Too small.");
|
|
}
|
|
var kh = await from.ReadValueUnmanagedAsync<KeyHeader>(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<AESKey>();
|
|
}
|
|
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<AESKey>(token);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
return await from.ReadValueUnmanagedAsync<AESKey>(token);
|
|
}
|
|
public static Task<AESKey> GenerateKey(Stream to, CancellationToken token = default) => GenerateKey(to, null, token);
|
|
public static async Task<AESKey> 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<KeyHeader>(kh, token);
|
|
|
|
using (var derive = new Rfc2898DeriveBytes(passwordProtect, salt))
|
|
{
|
|
AESKey pkey;
|
|
unsafe
|
|
{
|
|
pkey = derive.GetBytes(sizeof(AESKey)).ToUnmanaged<AESKey>();
|
|
}
|
|
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<AESKey>();
|
|
}
|
|
else
|
|
{
|
|
AESKey k;
|
|
await to.WriteValueUnmanagedAsync<KeyHeader>(KeyHeader.Create(), token);
|
|
await to.WriteValueUnmanagedAsync<AESKey>(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>(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<AESKey>();
|
|
|
|
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<FileHeader>(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<bool> IsEncryptedFile(Stream stream, CancellationToken token=default)
|
|
{
|
|
if (stream.Length < FileHeader.Size) return false;
|
|
var fh = await stream.ReadValueUnmanagedAsync<FileHeader>(token);
|
|
return fh.Valid;
|
|
}
|
|
|
|
public static async Task<bool> 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
|
|
}
|
|
}
|