mirror of
https://github.com/MagSec-fur/MagSec-Encrypter.git
synced 2026-06-01 12:25:00 +02:00
Harden encryption and add key UX improvements
Security: - Versioned, self-describing payload format (v2) that stores KDF parameters, enabling future crypto agility - Upgrade key derivation to PBKDF2-HMAC-SHA512 at 600k iterations (was SHA256 at 200k) - Bind the full header (prefix, version, KDF params, salt, nonce) into the AES-GCM tag as associated data so header tampering is detected - Zero derived keys and secret bytes from memory after use - Enforce a minimum combined key length - Keep backwards-compatible decryption for legacy v1 payloads UX: - Add a Generate button that creates a strong random key with a transcription-friendly alphabet - Add a Show/Hide keys toggle - Add a live key-strength indicator - Replace leftover Dutch file-dialog strings with English
This commit is contained in:
Generated
+43
-2
@@ -31,6 +31,9 @@ partial class Form1
|
||||
brandPictureBox = new PictureBox();
|
||||
keyRailPanel = new GlassPanel();
|
||||
sideInfoLabel = new Label();
|
||||
keyStrengthLabel = new Label();
|
||||
generateKeyButton = new NeonButton();
|
||||
toggleKeysButton = new NeonButton();
|
||||
key4TextBox = new TextBox();
|
||||
key4Label = new Label();
|
||||
key3TextBox = new TextBox();
|
||||
@@ -142,6 +145,9 @@ partial class Form1
|
||||
// keyRailPanel
|
||||
//
|
||||
keyRailPanel.Controls.Add(sideInfoLabel);
|
||||
keyRailPanel.Controls.Add(toggleKeysButton);
|
||||
keyRailPanel.Controls.Add(generateKeyButton);
|
||||
keyRailPanel.Controls.Add(keyStrengthLabel);
|
||||
keyRailPanel.Controls.Add(key4TextBox);
|
||||
keyRailPanel.Controls.Add(key4Label);
|
||||
keyRailPanel.Controls.Add(key3TextBox);
|
||||
@@ -160,12 +166,44 @@ partial class Form1
|
||||
// sideInfoLabel
|
||||
//
|
||||
sideInfoLabel.Font = new Font("Segoe UI", 9F);
|
||||
sideInfoLabel.Location = new Point(24, 409);
|
||||
sideInfoLabel.Location = new Point(24, 452);
|
||||
sideInfoLabel.Name = "sideInfoLabel";
|
||||
sideInfoLabel.Size = new Size(220, 56);
|
||||
sideInfoLabel.TabIndex = 10;
|
||||
sideInfoLabel.TabIndex = 13;
|
||||
sideInfoLabel.Text = "Use the same key combination for both text and image decryption later.";
|
||||
//
|
||||
// keyStrengthLabel
|
||||
//
|
||||
keyStrengthLabel.Font = new Font("Segoe UI Semibold", 9F, FontStyle.Bold);
|
||||
keyStrengthLabel.ForeColor = Color.FromArgb(148, 163, 184);
|
||||
keyStrengthLabel.Location = new Point(24, 376);
|
||||
keyStrengthLabel.Name = "keyStrengthLabel";
|
||||
keyStrengthLabel.Size = new Size(220, 18);
|
||||
keyStrengthLabel.TabIndex = 10;
|
||||
keyStrengthLabel.Text = "Strength: —";
|
||||
//
|
||||
// generateKeyButton
|
||||
//
|
||||
generateKeyButton.ForeColor = Color.FromArgb(245, 248, 255);
|
||||
generateKeyButton.Location = new Point(24, 402);
|
||||
generateKeyButton.Name = "generateKeyButton";
|
||||
generateKeyButton.Size = new Size(104, 38);
|
||||
generateKeyButton.TabIndex = 11;
|
||||
generateKeyButton.Text = "Generate";
|
||||
generateKeyButton.UseVisualStyleBackColor = false;
|
||||
generateKeyButton.Click += GenerateKeyButton_Click;
|
||||
//
|
||||
// toggleKeysButton
|
||||
//
|
||||
toggleKeysButton.ForeColor = Color.FromArgb(245, 248, 255);
|
||||
toggleKeysButton.Location = new Point(140, 402);
|
||||
toggleKeysButton.Name = "toggleKeysButton";
|
||||
toggleKeysButton.Size = new Size(104, 38);
|
||||
toggleKeysButton.TabIndex = 12;
|
||||
toggleKeysButton.Text = "Show keys";
|
||||
toggleKeysButton.UseVisualStyleBackColor = false;
|
||||
toggleKeysButton.Click += ToggleKeysButton_Click;
|
||||
//
|
||||
// key4TextBox
|
||||
//
|
||||
key4TextBox.Location = new Point(24, 344);
|
||||
@@ -564,6 +602,9 @@ partial class Form1
|
||||
private PictureBox brandPictureBox;
|
||||
private GlassPanel keyRailPanel;
|
||||
private Label sideInfoLabel;
|
||||
private Label keyStrengthLabel;
|
||||
private NeonButton generateKeyButton;
|
||||
private NeonButton toggleKeysButton;
|
||||
private TextBox key4TextBox;
|
||||
private Label key4Label;
|
||||
private TextBox key3TextBox;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -12,9 +13,22 @@ public partial class Form1 : Form
|
||||
private const int NonceSize = 12;
|
||||
private const int TagSize = 16;
|
||||
private const int KeySize = 32;
|
||||
private const int Pbkdf2Iterations = 200_000;
|
||||
private const string TextPrefix = "TXT1";
|
||||
private const string ImagePrefix = "IMG1";
|
||||
private const int Pbkdf2Iterations = 600_000;
|
||||
private const int LegacyPbkdf2Iterations = 200_000;
|
||||
private const int MinimumKeyLength = 8;
|
||||
private const byte FormatVersion = 2;
|
||||
private const byte KdfSha256 = 1;
|
||||
private const byte KdfSha512 = 2;
|
||||
|
||||
// V2 self-describing format. V1 prefixes are still decryptable for backwards compatibility.
|
||||
private const string TextPrefix = "TXT2";
|
||||
private const string ImagePrefix = "IMG2";
|
||||
private const string LegacyTextPrefix = "TXT1";
|
||||
private const string LegacyImagePrefix = "IMG1";
|
||||
|
||||
// prefix(4) + version(1) + kdfId(1) + iterations(4) + salt(16) + nonce(12)
|
||||
private const int V2HeaderLength = 4 + 1 + 1 + 4 + SaltSize + NonceSize;
|
||||
private const int LegacyHeaderLength = 4 + SaltSize + NonceSize + TagSize;
|
||||
|
||||
private static readonly Color VoidColor = Color.FromArgb(3, 0, 20);
|
||||
private static readonly Color PanelColor = Color.FromArgb(12, 17, 39);
|
||||
@@ -32,6 +46,7 @@ public partial class Form1 : Form
|
||||
private float nebulaPhase;
|
||||
private string? selectedImagePath;
|
||||
private string? selectedEncryptedImagePath;
|
||||
private bool keysVisible;
|
||||
|
||||
public Form1()
|
||||
{
|
||||
@@ -39,9 +54,20 @@ public partial class Form1 : Form
|
||||
ApplyTheme();
|
||||
LoadBrandImage();
|
||||
ConfigureDragAndDrop();
|
||||
ConfigureKeyFeedback();
|
||||
InitializeStarfield();
|
||||
}
|
||||
|
||||
private void ConfigureKeyFeedback()
|
||||
{
|
||||
foreach (TextBox keyBox in new[] { key1TextBox, key2TextBox, key3TextBox, key4TextBox })
|
||||
{
|
||||
keyBox.TextChanged += (_, _) => UpdateKeyStrength();
|
||||
}
|
||||
|
||||
UpdateKeyStrength();
|
||||
}
|
||||
|
||||
private void ApplyTheme()
|
||||
{
|
||||
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint, true);
|
||||
@@ -61,6 +87,9 @@ public partial class Form1 : Form
|
||||
ConfigureButton(encryptImageButton, TealColor);
|
||||
ConfigureButton(selectEncryptedImageButton, IndigoColor);
|
||||
ConfigureButton(decryptImageButton, CyanColor);
|
||||
ConfigureButton(generateKeyButton, EmeraldColor);
|
||||
ConfigureButton(toggleKeysButton, Color.FromArgb(36, 45, 72), isSecondary: true);
|
||||
toggleKeysButton.GlowColor = CyanColor;
|
||||
|
||||
StyleTextBox(key1TextBox);
|
||||
StyleTextBox(key2TextBox);
|
||||
@@ -297,7 +326,7 @@ public partial class Form1 : Form
|
||||
}
|
||||
|
||||
byte[] payload = Convert.FromBase64String(inputTextBox.Text.Trim());
|
||||
byte[] plain = DecryptBytes(payload, secret, TextPrefix);
|
||||
byte[] plain = DecryptBytes(payload, secret, TextPrefix, LegacyTextPrefix);
|
||||
outputTextBox.Text = Encoding.UTF8.GetString(plain);
|
||||
ShowStatus("Text decrypted successfully.", isError: false);
|
||||
}
|
||||
@@ -339,11 +368,76 @@ public partial class Form1 : Form
|
||||
ShowStatus("Workspace cleared.", isError: false);
|
||||
}
|
||||
|
||||
private void GenerateKeyButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
TextBox target = new[] { key1TextBox, key2TextBox, key3TextBox, key4TextBox }
|
||||
.FirstOrDefault(box => string.IsNullOrWhiteSpace(box.Text)) ?? key1TextBox;
|
||||
|
||||
target.Text = GenerateStrongKey(24);
|
||||
|
||||
if (!keysVisible)
|
||||
{
|
||||
SetKeysVisible(true);
|
||||
}
|
||||
|
||||
target.Focus();
|
||||
ShowStatus("Strong key generated. Store it somewhere safe.", isError: false);
|
||||
}
|
||||
|
||||
private void ToggleKeysButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
SetKeysVisible(!keysVisible);
|
||||
}
|
||||
|
||||
private void SetKeysVisible(bool visible)
|
||||
{
|
||||
keysVisible = visible;
|
||||
char passwordChar = visible ? '\0' : '*';
|
||||
foreach (TextBox keyBox in new[] { key1TextBox, key2TextBox, key3TextBox, key4TextBox })
|
||||
{
|
||||
keyBox.PasswordChar = passwordChar;
|
||||
}
|
||||
|
||||
toggleKeysButton.Text = visible ? "Hide keys" : "Show keys";
|
||||
}
|
||||
|
||||
private static string GenerateStrongKey(int length)
|
||||
{
|
||||
// Unambiguous alphabet (no 0/O/1/l/I) so generated keys are easy to transcribe.
|
||||
const string alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*-_=+";
|
||||
char[] result = new char[length];
|
||||
for (int index = 0; index < length; index++)
|
||||
{
|
||||
result[index] = alphabet[RandomNumberGenerator.GetInt32(alphabet.Length)];
|
||||
}
|
||||
|
||||
return new string(result);
|
||||
}
|
||||
|
||||
private void UpdateKeyStrength()
|
||||
{
|
||||
int totalLength = new[] { key1TextBox.Text, key2TextBox.Text, key3TextBox.Text, key4TextBox.Text }
|
||||
.Where(key => !string.IsNullOrWhiteSpace(key))
|
||||
.Sum(key => key.Trim().Length);
|
||||
|
||||
(string label, Color color) = totalLength switch
|
||||
{
|
||||
0 => ("Strength: —", TextMutedColor),
|
||||
< MinimumKeyLength => ("Strength: Weak", DangerColor),
|
||||
< 16 => ("Strength: Fair", Color.FromArgb(250, 204, 21)),
|
||||
< 28 => ("Strength: Strong", EmeraldColor),
|
||||
_ => ("Strength: Excellent", TealColor)
|
||||
};
|
||||
|
||||
keyStrengthLabel.Text = label;
|
||||
keyStrengthLabel.ForeColor = color;
|
||||
}
|
||||
|
||||
private void SelectImageButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
using OpenFileDialog dialog = new()
|
||||
{
|
||||
Filter = "Afbeeldingen|*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.webp|Alle bestanden|*.*",
|
||||
Filter = "Images|*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.webp|All files|*.*",
|
||||
Title = "Select an image"
|
||||
};
|
||||
|
||||
@@ -395,7 +489,7 @@ public partial class Form1 : Form
|
||||
{
|
||||
using OpenFileDialog dialog = new()
|
||||
{
|
||||
Filter = "MagSec encrypted image|*.mseimg|Alle bestanden|*.*",
|
||||
Filter = "MagSec encrypted image|*.mseimg|All files|*.*",
|
||||
Title = "Select an encrypted image"
|
||||
};
|
||||
|
||||
@@ -417,12 +511,12 @@ public partial class Form1 : Form
|
||||
}
|
||||
|
||||
byte[] encrypted = File.ReadAllBytes(selectedEncryptedImagePath);
|
||||
byte[] decryptedPackage = DecryptBytes(encrypted, secret, ImagePrefix);
|
||||
byte[] decryptedPackage = DecryptBytes(encrypted, secret, ImagePrefix, LegacyImagePrefix);
|
||||
(string extension, byte[] imageBytes) = ReadImagePackage(decryptedPackage);
|
||||
|
||||
using SaveFileDialog dialog = new()
|
||||
{
|
||||
Filter = $"Origineel bestand|*{extension}|Alle bestanden|*.*",
|
||||
Filter = $"Original file|*{extension}|All files|*.*",
|
||||
FileName = $"{Path.GetFileNameWithoutExtension(selectedEncryptedImagePath)}_restored{extension}",
|
||||
Title = "Save decrypted image"
|
||||
};
|
||||
@@ -462,6 +556,11 @@ public partial class Form1 : Form
|
||||
throw new InvalidOperationException("Enter at least one key.");
|
||||
}
|
||||
|
||||
if (filledKeys.Sum(key => key.Length) < MinimumKeyLength)
|
||||
{
|
||||
throw new InvalidOperationException($"Use at least {MinimumKeyLength} characters across your keys for a secure result.");
|
||||
}
|
||||
|
||||
return string.Join("|", filledKeys);
|
||||
}
|
||||
|
||||
@@ -469,42 +568,114 @@ public partial class Form1 : Form
|
||||
{
|
||||
byte[] salt = RandomNumberGenerator.GetBytes(SaltSize);
|
||||
byte[] nonce = RandomNumberGenerator.GetBytes(NonceSize);
|
||||
byte[] key = DeriveKey(secret, salt);
|
||||
byte[] cipherBytes = new byte[plainBytes.Length];
|
||||
byte[] tag = new byte[TagSize];
|
||||
byte[] key = DeriveKey(secret, salt, Pbkdf2Iterations, KdfSha512);
|
||||
|
||||
using var aes = new AesGcm(key, TagSize);
|
||||
aes.Encrypt(nonce, plainBytes, cipherBytes, tag);
|
||||
|
||||
byte[] payload = new byte[4 + SaltSize + NonceSize + TagSize + cipherBytes.Length];
|
||||
byte[] payload = new byte[V2HeaderLength + TagSize + plainBytes.Length];
|
||||
Encoding.ASCII.GetBytes(prefix).CopyTo(payload, 0);
|
||||
salt.CopyTo(payload, 4);
|
||||
nonce.CopyTo(payload, 4 + SaltSize);
|
||||
tag.CopyTo(payload, 4 + SaltSize + NonceSize);
|
||||
cipherBytes.CopyTo(payload, 4 + SaltSize + NonceSize + TagSize);
|
||||
payload[4] = FormatVersion;
|
||||
payload[5] = KdfSha512;
|
||||
BinaryPrimitives.WriteInt32BigEndian(payload.AsSpan(6, 4), Pbkdf2Iterations);
|
||||
salt.CopyTo(payload, 10);
|
||||
nonce.CopyTo(payload, 10 + SaltSize);
|
||||
|
||||
// The whole header (prefix, version, KDF params, salt, nonce) is bound into the
|
||||
// authentication tag as associated data so tampering with it is detected on decrypt.
|
||||
ReadOnlySpan<byte> associatedData = payload.AsSpan(0, V2HeaderLength);
|
||||
Span<byte> tag = payload.AsSpan(V2HeaderLength, TagSize);
|
||||
Span<byte> cipherBytes = payload.AsSpan(V2HeaderLength + TagSize);
|
||||
|
||||
try
|
||||
{
|
||||
using var aes = new AesGcm(key, TagSize);
|
||||
aes.Encrypt(nonce, plainBytes, cipherBytes, tag, associatedData);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CryptographicOperations.ZeroMemory(key);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
private static byte[] DecryptBytes(byte[] payload, string secret, string expectedPrefix)
|
||||
private static byte[] DecryptBytes(byte[] payload, string secret, string expectedPrefix, string legacyPrefix)
|
||||
{
|
||||
int headerLength = 4 + SaltSize + NonceSize + TagSize;
|
||||
if (payload.Length <= headerLength)
|
||||
if (payload.Length < 4)
|
||||
{
|
||||
throw new InvalidOperationException("The encrypted content is incomplete.");
|
||||
}
|
||||
|
||||
string prefix = Encoding.ASCII.GetString(payload, 0, 4);
|
||||
if (!string.Equals(prefix, expectedPrefix, StringComparison.Ordinal))
|
||||
if (string.Equals(prefix, expectedPrefix, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException("Unknown encryption format.");
|
||||
return DecryptV2(payload, secret);
|
||||
}
|
||||
|
||||
if (string.Equals(prefix, legacyPrefix, StringComparison.Ordinal))
|
||||
{
|
||||
return DecryptLegacy(payload, secret);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Unknown encryption format.");
|
||||
}
|
||||
|
||||
private static byte[] DecryptV2(byte[] payload, string secret)
|
||||
{
|
||||
if (payload.Length < V2HeaderLength + TagSize)
|
||||
{
|
||||
throw new InvalidOperationException("The encrypted content is incomplete.");
|
||||
}
|
||||
|
||||
byte version = payload[4];
|
||||
if (version != FormatVersion)
|
||||
{
|
||||
throw new InvalidOperationException("Unsupported encryption version.");
|
||||
}
|
||||
|
||||
byte kdfId = payload[5];
|
||||
int iterations = BinaryPrimitives.ReadInt32BigEndian(payload.AsSpan(6, 4));
|
||||
if (iterations < 1 || iterations > 10_000_000)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid key derivation parameters.");
|
||||
}
|
||||
|
||||
byte[] salt = payload[10..(10 + SaltSize)];
|
||||
byte[] nonce = payload[(10 + SaltSize)..V2HeaderLength];
|
||||
byte[] tag = payload[V2HeaderLength..(V2HeaderLength + TagSize)];
|
||||
byte[] cipherBytes = payload[(V2HeaderLength + TagSize)..];
|
||||
byte[] plainBytes = new byte[cipherBytes.Length];
|
||||
byte[] key = DeriveKey(secret, salt, iterations, kdfId);
|
||||
|
||||
try
|
||||
{
|
||||
using var aes = new AesGcm(key, TagSize);
|
||||
aes.Decrypt(nonce, cipherBytes, tag, plainBytes, payload.AsSpan(0, V2HeaderLength));
|
||||
}
|
||||
catch (CryptographicException)
|
||||
{
|
||||
CryptographicOperations.ZeroMemory(plainBytes);
|
||||
throw new InvalidOperationException("The keys do not match or the data is corrupted.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
CryptographicOperations.ZeroMemory(key);
|
||||
}
|
||||
|
||||
return plainBytes;
|
||||
}
|
||||
|
||||
private static byte[] DecryptLegacy(byte[] payload, string secret)
|
||||
{
|
||||
if (payload.Length <= LegacyHeaderLength)
|
||||
{
|
||||
throw new InvalidOperationException("The encrypted content is incomplete.");
|
||||
}
|
||||
|
||||
byte[] salt = payload[4..(4 + SaltSize)];
|
||||
byte[] nonce = payload[(4 + SaltSize)..(4 + SaltSize + NonceSize)];
|
||||
byte[] tag = payload[(4 + SaltSize + NonceSize)..headerLength];
|
||||
byte[] cipherBytes = payload[headerLength..];
|
||||
byte[] tag = payload[(4 + SaltSize + NonceSize)..LegacyHeaderLength];
|
||||
byte[] cipherBytes = payload[LegacyHeaderLength..];
|
||||
byte[] plainBytes = new byte[cipherBytes.Length];
|
||||
byte[] key = DeriveKey(secret, salt);
|
||||
byte[] key = DeriveKey(secret, salt, LegacyPbkdf2Iterations, KdfSha256);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -513,8 +684,13 @@ public partial class Form1 : Form
|
||||
}
|
||||
catch (CryptographicException)
|
||||
{
|
||||
CryptographicOperations.ZeroMemory(plainBytes);
|
||||
throw new InvalidOperationException("The keys do not match or the data is corrupted.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
CryptographicOperations.ZeroMemory(key);
|
||||
}
|
||||
|
||||
return plainBytes;
|
||||
}
|
||||
@@ -549,14 +725,19 @@ public partial class Form1 : Form
|
||||
return (extension, imageBytes);
|
||||
}
|
||||
|
||||
private static byte[] DeriveKey(string secret, byte[] salt)
|
||||
private static byte[] DeriveKey(string secret, byte[] salt, int iterations, byte kdfId)
|
||||
{
|
||||
return Rfc2898DeriveBytes.Pbkdf2(
|
||||
Encoding.UTF8.GetBytes(secret),
|
||||
salt,
|
||||
Pbkdf2Iterations,
|
||||
HashAlgorithmName.SHA256,
|
||||
KeySize);
|
||||
HashAlgorithmName hash = kdfId == KdfSha512 ? HashAlgorithmName.SHA512 : HashAlgorithmName.SHA256;
|
||||
byte[] secretBytes = Encoding.UTF8.GetBytes(secret);
|
||||
|
||||
try
|
||||
{
|
||||
return Rfc2898DeriveBytes.Pbkdf2(secretBytes, salt, iterations, hash, KeySize);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CryptographicOperations.ZeroMemory(secretBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowStatus(string message, bool isError)
|
||||
|
||||
Reference in New Issue
Block a user