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:
Claude
2026-06-01 10:08:31 +00:00
parent 96becf3607
commit 4744315fdd
2 changed files with 260 additions and 38 deletions
+45 -4
View File
@@ -31,6 +31,9 @@ partial class Form1
brandPictureBox = new PictureBox(); brandPictureBox = new PictureBox();
keyRailPanel = new GlassPanel(); keyRailPanel = new GlassPanel();
sideInfoLabel = new Label(); sideInfoLabel = new Label();
keyStrengthLabel = new Label();
generateKeyButton = new NeonButton();
toggleKeysButton = new NeonButton();
key4TextBox = new TextBox(); key4TextBox = new TextBox();
key4Label = new Label(); key4Label = new Label();
key3TextBox = new TextBox(); key3TextBox = new TextBox();
@@ -142,6 +145,9 @@ partial class Form1
// keyRailPanel // keyRailPanel
// //
keyRailPanel.Controls.Add(sideInfoLabel); keyRailPanel.Controls.Add(sideInfoLabel);
keyRailPanel.Controls.Add(toggleKeysButton);
keyRailPanel.Controls.Add(generateKeyButton);
keyRailPanel.Controls.Add(keyStrengthLabel);
keyRailPanel.Controls.Add(key4TextBox); keyRailPanel.Controls.Add(key4TextBox);
keyRailPanel.Controls.Add(key4Label); keyRailPanel.Controls.Add(key4Label);
keyRailPanel.Controls.Add(key3TextBox); keyRailPanel.Controls.Add(key3TextBox);
@@ -156,15 +162,47 @@ partial class Form1
keyRailPanel.Name = "keyRailPanel"; keyRailPanel.Name = "keyRailPanel";
keyRailPanel.Size = new Size(282, 526); keyRailPanel.Size = new Size(282, 526);
keyRailPanel.TabIndex = 1; keyRailPanel.TabIndex = 1;
// //
// sideInfoLabel // sideInfoLabel
// //
sideInfoLabel.Font = new Font("Segoe UI", 9F); sideInfoLabel.Font = new Font("Segoe UI", 9F);
sideInfoLabel.Location = new Point(24, 409); sideInfoLabel.Location = new Point(24, 452);
sideInfoLabel.Name = "sideInfoLabel"; sideInfoLabel.Name = "sideInfoLabel";
sideInfoLabel.Size = new Size(220, 56); 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."; 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
// //
@@ -564,6 +602,9 @@ partial class Form1
private PictureBox brandPictureBox; private PictureBox brandPictureBox;
private GlassPanel keyRailPanel; private GlassPanel keyRailPanel;
private Label sideInfoLabel; private Label sideInfoLabel;
private Label keyStrengthLabel;
private NeonButton generateKeyButton;
private NeonButton toggleKeysButton;
private TextBox key4TextBox; private TextBox key4TextBox;
private Label key4Label; private Label key4Label;
private TextBox key3TextBox; private TextBox key3TextBox;
+215 -34
View File
@@ -1,3 +1,4 @@
using System.Buffers.Binary;
using System.ComponentModel; using System.ComponentModel;
using System.Drawing.Drawing2D; using System.Drawing.Drawing2D;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@@ -12,9 +13,22 @@ public partial class Form1 : Form
private const int NonceSize = 12; private const int NonceSize = 12;
private const int TagSize = 16; private const int TagSize = 16;
private const int KeySize = 32; private const int KeySize = 32;
private const int Pbkdf2Iterations = 200_000; private const int Pbkdf2Iterations = 600_000;
private const string TextPrefix = "TXT1"; private const int LegacyPbkdf2Iterations = 200_000;
private const string ImagePrefix = "IMG1"; 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 VoidColor = Color.FromArgb(3, 0, 20);
private static readonly Color PanelColor = Color.FromArgb(12, 17, 39); private static readonly Color PanelColor = Color.FromArgb(12, 17, 39);
@@ -32,6 +46,7 @@ public partial class Form1 : Form
private float nebulaPhase; private float nebulaPhase;
private string? selectedImagePath; private string? selectedImagePath;
private string? selectedEncryptedImagePath; private string? selectedEncryptedImagePath;
private bool keysVisible;
public Form1() public Form1()
{ {
@@ -39,9 +54,20 @@ public partial class Form1 : Form
ApplyTheme(); ApplyTheme();
LoadBrandImage(); LoadBrandImage();
ConfigureDragAndDrop(); ConfigureDragAndDrop();
ConfigureKeyFeedback();
InitializeStarfield(); InitializeStarfield();
} }
private void ConfigureKeyFeedback()
{
foreach (TextBox keyBox in new[] { key1TextBox, key2TextBox, key3TextBox, key4TextBox })
{
keyBox.TextChanged += (_, _) => UpdateKeyStrength();
}
UpdateKeyStrength();
}
private void ApplyTheme() private void ApplyTheme()
{ {
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint, true); SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint, true);
@@ -61,6 +87,9 @@ public partial class Form1 : Form
ConfigureButton(encryptImageButton, TealColor); ConfigureButton(encryptImageButton, TealColor);
ConfigureButton(selectEncryptedImageButton, IndigoColor); ConfigureButton(selectEncryptedImageButton, IndigoColor);
ConfigureButton(decryptImageButton, CyanColor); ConfigureButton(decryptImageButton, CyanColor);
ConfigureButton(generateKeyButton, EmeraldColor);
ConfigureButton(toggleKeysButton, Color.FromArgb(36, 45, 72), isSecondary: true);
toggleKeysButton.GlowColor = CyanColor;
StyleTextBox(key1TextBox); StyleTextBox(key1TextBox);
StyleTextBox(key2TextBox); StyleTextBox(key2TextBox);
@@ -297,7 +326,7 @@ public partial class Form1 : Form
} }
byte[] payload = Convert.FromBase64String(inputTextBox.Text.Trim()); 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); outputTextBox.Text = Encoding.UTF8.GetString(plain);
ShowStatus("Text decrypted successfully.", isError: false); ShowStatus("Text decrypted successfully.", isError: false);
} }
@@ -339,11 +368,76 @@ public partial class Form1 : Form
ShowStatus("Workspace cleared.", isError: false); 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) private void SelectImageButton_Click(object sender, EventArgs e)
{ {
using OpenFileDialog dialog = new() 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" Title = "Select an image"
}; };
@@ -395,7 +489,7 @@ public partial class Form1 : Form
{ {
using OpenFileDialog dialog = new() using OpenFileDialog dialog = new()
{ {
Filter = "MagSec encrypted image|*.mseimg|Alle bestanden|*.*", Filter = "MagSec encrypted image|*.mseimg|All files|*.*",
Title = "Select an encrypted image" Title = "Select an encrypted image"
}; };
@@ -417,12 +511,12 @@ public partial class Form1 : Form
} }
byte[] encrypted = File.ReadAllBytes(selectedEncryptedImagePath); byte[] encrypted = File.ReadAllBytes(selectedEncryptedImagePath);
byte[] decryptedPackage = DecryptBytes(encrypted, secret, ImagePrefix); byte[] decryptedPackage = DecryptBytes(encrypted, secret, ImagePrefix, LegacyImagePrefix);
(string extension, byte[] imageBytes) = ReadImagePackage(decryptedPackage); (string extension, byte[] imageBytes) = ReadImagePackage(decryptedPackage);
using SaveFileDialog dialog = new() using SaveFileDialog dialog = new()
{ {
Filter = $"Origineel bestand|*{extension}|Alle bestanden|*.*", Filter = $"Original file|*{extension}|All files|*.*",
FileName = $"{Path.GetFileNameWithoutExtension(selectedEncryptedImagePath)}_restored{extension}", FileName = $"{Path.GetFileNameWithoutExtension(selectedEncryptedImagePath)}_restored{extension}",
Title = "Save decrypted image" Title = "Save decrypted image"
}; };
@@ -462,6 +556,11 @@ public partial class Form1 : Form
throw new InvalidOperationException("Enter at least one key."); 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); return string.Join("|", filledKeys);
} }
@@ -469,42 +568,114 @@ public partial class Form1 : Form
{ {
byte[] salt = RandomNumberGenerator.GetBytes(SaltSize); byte[] salt = RandomNumberGenerator.GetBytes(SaltSize);
byte[] nonce = RandomNumberGenerator.GetBytes(NonceSize); byte[] nonce = RandomNumberGenerator.GetBytes(NonceSize);
byte[] key = DeriveKey(secret, salt); byte[] key = DeriveKey(secret, salt, Pbkdf2Iterations, KdfSha512);
byte[] cipherBytes = new byte[plainBytes.Length];
byte[] tag = new byte[TagSize];
using var aes = new AesGcm(key, TagSize); byte[] payload = new byte[V2HeaderLength + TagSize + plainBytes.Length];
aes.Encrypt(nonce, plainBytes, cipherBytes, tag);
byte[] payload = new byte[4 + SaltSize + NonceSize + TagSize + cipherBytes.Length];
Encoding.ASCII.GetBytes(prefix).CopyTo(payload, 0); Encoding.ASCII.GetBytes(prefix).CopyTo(payload, 0);
salt.CopyTo(payload, 4); payload[4] = FormatVersion;
nonce.CopyTo(payload, 4 + SaltSize); payload[5] = KdfSha512;
tag.CopyTo(payload, 4 + SaltSize + NonceSize); BinaryPrimitives.WriteInt32BigEndian(payload.AsSpan(6, 4), Pbkdf2Iterations);
cipherBytes.CopyTo(payload, 4 + SaltSize + NonceSize + TagSize); 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; 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 < 4)
if (payload.Length <= headerLength)
{ {
throw new InvalidOperationException("The encrypted content is incomplete."); throw new InvalidOperationException("The encrypted content is incomplete.");
} }
string prefix = Encoding.ASCII.GetString(payload, 0, 4); 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[] salt = payload[4..(4 + SaltSize)];
byte[] nonce = payload[(4 + SaltSize)..(4 + SaltSize + NonceSize)]; byte[] nonce = payload[(4 + SaltSize)..(4 + SaltSize + NonceSize)];
byte[] tag = payload[(4 + SaltSize + NonceSize)..headerLength]; byte[] tag = payload[(4 + SaltSize + NonceSize)..LegacyHeaderLength];
byte[] cipherBytes = payload[headerLength..]; byte[] cipherBytes = payload[LegacyHeaderLength..];
byte[] plainBytes = new byte[cipherBytes.Length]; byte[] plainBytes = new byte[cipherBytes.Length];
byte[] key = DeriveKey(secret, salt); byte[] key = DeriveKey(secret, salt, LegacyPbkdf2Iterations, KdfSha256);
try try
{ {
@@ -513,8 +684,13 @@ public partial class Form1 : Form
} }
catch (CryptographicException) catch (CryptographicException)
{ {
CryptographicOperations.ZeroMemory(plainBytes);
throw new InvalidOperationException("The keys do not match or the data is corrupted."); throw new InvalidOperationException("The keys do not match or the data is corrupted.");
} }
finally
{
CryptographicOperations.ZeroMemory(key);
}
return plainBytes; return plainBytes;
} }
@@ -549,14 +725,19 @@ public partial class Form1 : Form
return (extension, imageBytes); 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( HashAlgorithmName hash = kdfId == KdfSha512 ? HashAlgorithmName.SHA512 : HashAlgorithmName.SHA256;
Encoding.UTF8.GetBytes(secret), byte[] secretBytes = Encoding.UTF8.GetBytes(secret);
salt,
Pbkdf2Iterations, try
HashAlgorithmName.SHA256, {
KeySize); return Rfc2898DeriveBytes.Pbkdf2(secretBytes, salt, iterations, hash, KeySize);
}
finally
{
CryptographicOperations.ZeroMemory(secretBytes);
}
} }
private void ShowStatus(string message, bool isError) private void ShowStatus(string message, bool isError)