Skip to content

Commit 239b5b6

Browse files
committed
Refactor password encryption with AES-256 and PBKDF2
Replaced the old `StringCipher` encryption with a new `SecurePasswordManager` class using AES-256 and PBKDF2 for machine-specific password encryption. Added support for automatic migration of passwords from the old format to the new secure method. Enhanced error handling, logging, and user prompts for decryption failures. Updated `MainForm.cs` to integrate the new encryption system and handle password migration. Improved configuration persistence by saving migrated passwords in the new format. Documented changes in `CHANGELOG.md` and added `SecurePasswordManager.cs` to the project. Ensured backward compatibility and compatibility with `.NET Framework 4.8`. These changes improve security, reliability, and maintainability, particularly for environments requiring robust certificate and password management.
1 parent 2aaec6d commit 239b5b6

4 files changed

Lines changed: 327 additions & 5 deletions

File tree

CHANGELOG.MD

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,60 @@
66
- New `TimestampServerEditForm` for adding and editing individual timestamp servers
77
- New `TimestampServerManagementForm` for centralized server configuration management
88
- Added `TimestampServer` and `TimestampManager` classes for server handling and orchestration
9+
- Dynamic interface adaptation: "Timestamp Servers" for PFX/Certificate Store and "Endpoints" for Trusted Signing
910
- Built-in timestamp server availability testing and health monitoring
1011
- Support for server prioritization, enabling/disabling, and timeout configuration
12+
- Added certificate type persistence - application now remembers your preferred signing method (Windows Certificate Store, PFX Certificate, or Trusted Signing)
1113

1214
### 🎨 User Interface Enhancements
1315
- Enhanced MainForm UI with new menu options for certificate monitoring and timestamp server management
1416
- Introduced color-coded alerts for certificate expiry in both Windows Certificate Store and PFX scenarios
1517
- Improved certificate information display with better visual feedback
1618
- Added intuitive forms for managing timestamp server configurations
19+
- Context-aware UI labels that change based on signing type (Trusted Signing vs. traditional methods)
20+
21+
### 🔒 Security Improvements
22+
- **Major Security Enhancement**: Completely redesigned password encryption system
23+
- Replaced hardcoded encryption keys with machine-specific key derivation
24+
- Upgraded from basic encryption to AES-256 with PBKDF2 key derivation (100,000 iterations)
25+
- Implemented automatic migration from old encryption format to new secure method
26+
- Added machine-specific entropy sources (hardware identifiers, system properties)
27+
- Passwords encrypted on one machine cannot be decrypted on another (intentional security feature)
28+
- Enhanced certificate validation and password security handling
1729

1830
### 🏗️ Architecture Improvements
1931
- Refactored signing classes (`SignerPfx`, `SignerThumbprint`, `SignerTrustedSigning`) to inherit from new `SignerBase` abstract class
2032
- Centralized common signing logic, reducing code redundancy and improving maintainability
33+
- Added new `SecurePasswordManager` class for robust password encryption/decryption
2134
- Enhanced certificate validation and monitoring capabilities
2235
- Improved error handling and validation for certificate paths and passwords
36+
- Better separation of concerns with dedicated security and configuration management classes
2337

2438
### ⚡ Performance & Reliability
2539
- Implemented asynchronous operations for better application responsiveness
2640
- Enhanced logging system for improved troubleshooting and debugging
2741
- Added automatic failover to backup timestamp servers when primary servers are unavailable
2842
- Improved stability when handling certificate operations and network-related timestamp failures
43+
- Better configuration persistence and loading mechanisms
2944

3045
### 🐛 Bug Fixes
3146
- Better error recovery for network-related timestamp failures
3247
- Enhanced validation for certificate operations
3348
- Improved stability in certificate monitoring scenarios
49+
- Fixed configuration loading order to prevent UI overrides
50+
- Better handling of corrupted or incompatible password data
51+
52+
### 🔧 Technical Details
53+
- Enhanced compatibility with .NET Framework 4.8
54+
- Improved machine-specific key generation using multiple entropy sources
55+
- Added comprehensive error handling and logging for security operations
56+
- Backward compatibility maintained through automatic password migration system
3457

3558
---
3659

37-
*This release significantly enhances the reliability, user experience, and enterprise-readiness of the SignTool GUI, particularly for environments requiring robust certificate management and timestamping
60+
*This release represents a major milestone in security and usability, significantly enhancing the reliability, user experience, and enterprise-readiness of the SignTool GUI. The new security architecture ensures that sensitive certificate passwords are protected with industry-standard encryption while maintaining seamless user experience through automatic migration and intelligent configuration management.*
61+
62+
---
3863

3964
## Version 1.4.0.0 (17-03-2025):
4065

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Security.Cryptography;
5+
using System.Text;
6+
using static SignToolGUI.Class.FileLogger;
7+
8+
namespace SignToolGUI.Class
9+
{
10+
/// <summary>
11+
/// Provides secure password encryption/decryption using machine-specific keys
12+
/// </summary>
13+
public static class SecurePasswordManager
14+
{
15+
private const int KeySize = 256; // AES-256
16+
private const int IvSize = 128; // AES block size
17+
private const int SaltSize = 256; // Salt size in bits
18+
private const int DerivationIterations = 100000; // PBKDF2 iterations (increased for better security)
19+
20+
/// <summary>
21+
/// Generates a machine-specific encryption key based on hardware identifiers
22+
/// </summary>
23+
/// <returns>Base64 encoded machine-specific key</returns>
24+
private static string GenerateMachineSpecificKey()
25+
{
26+
try
27+
{
28+
var machineInfo = new StringBuilder();
29+
30+
// Combine multiple hardware/system identifiers for uniqueness
31+
machineInfo.Append(Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER") ?? "UNKNOWN_PROCESSOR");
32+
machineInfo.Append(Environment.MachineName);
33+
machineInfo.Append(Environment.OSVersion.Platform.ToString());
34+
machineInfo.Append(Environment.OSVersion.Version.ToString());
35+
machineInfo.Append(Environment.UserDomainName);
36+
machineInfo.Append(Environment.Is64BitOperatingSystem ? "x64" : "x86");
37+
38+
// Add additional entropy from system drive serial (if available)
39+
try
40+
{
41+
var systemDrive = Environment.SystemDirectory.Substring(0, 3);
42+
var driveInfo = new DriveInfo(systemDrive);
43+
if (driveInfo.IsReady)
44+
{
45+
// Note: VolumeLabel might not always be available, but we try
46+
machineInfo.Append(driveInfo.DriveFormat);
47+
machineInfo.Append(driveInfo.TotalSize.ToString());
48+
}
49+
}
50+
catch
51+
{
52+
// Ignore drive info errors, we have other entropy sources
53+
machineInfo.Append("DEFAULT_DRIVE_ENTROPY");
54+
}
55+
56+
// Create a hash of the combined information
57+
using (var sha256 = SHA256.Create())
58+
{
59+
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(machineInfo.ToString()));
60+
return Convert.ToBase64String(hash);
61+
}
62+
}
63+
catch (Exception ex)
64+
{
65+
Message($"Error generating machine-specific key: {ex.Message}", EventType.Error, 3031);
66+
// Fallback to a machine name based key if hardware info is unavailable
67+
using (var sha256 = SHA256.Create())
68+
{
69+
var fallbackData = $"FALLBACK_{Environment.MachineName}_{Environment.UserName}";
70+
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(fallbackData));
71+
return Convert.ToBase64String(hash);
72+
}
73+
}
74+
}
75+
76+
/// <summary>
77+
/// Encrypts a password using AES with machine-specific key derivation
78+
/// </summary>
79+
/// <param name="plainText">Password to encrypt</param>
80+
/// <returns>Base64 encoded encrypted password with metadata</returns>
81+
public static string EncryptPassword(string plainText)
82+
{
83+
if (string.IsNullOrEmpty(plainText))
84+
return string.Empty;
85+
86+
try
87+
{
88+
var machineKey = GenerateMachineSpecificKey();
89+
var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
90+
91+
// Generate random salt and IV
92+
var salt = GenerateRandomBytes(SaltSize / 8);
93+
var iv = GenerateRandomBytes(IvSize / 8);
94+
95+
// Derive key from machine-specific data and salt
96+
using (var pbkdf2 = new Rfc2898DeriveBytes(machineKey, salt, DerivationIterations))
97+
{
98+
var keyBytes = pbkdf2.GetBytes(KeySize / 8);
99+
100+
using (var aes = Aes.Create())
101+
{
102+
aes.KeySize = KeySize;
103+
aes.BlockSize = IvSize;
104+
aes.Mode = CipherMode.CBC;
105+
aes.Padding = PaddingMode.PKCS7;
106+
aes.Key = keyBytes;
107+
aes.IV = iv;
108+
109+
using (var encryptor = aes.CreateEncryptor())
110+
using (var msEncrypt = new MemoryStream())
111+
{
112+
// Write salt and IV first
113+
msEncrypt.Write(salt, 0, salt.Length);
114+
msEncrypt.Write(iv, 0, iv.Length);
115+
116+
// Encrypt the password
117+
using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
118+
{
119+
csEncrypt.Write(plainTextBytes, 0, plainTextBytes.Length);
120+
csEncrypt.FlushFinalBlock();
121+
}
122+
123+
return Convert.ToBase64String(msEncrypt.ToArray());
124+
}
125+
}
126+
}
127+
}
128+
catch (Exception ex)
129+
{
130+
Message($"Error encrypting password: {ex.Message}", EventType.Error, 3032);
131+
throw new InvalidOperationException("Failed to encrypt password", ex);
132+
}
133+
}
134+
135+
/// <summary>
136+
/// Decrypts a password using AES with machine-specific key derivation
137+
/// </summary>
138+
/// <param name="cipherText">Base64 encoded encrypted password</param>
139+
/// <returns>Decrypted password</returns>
140+
public static string DecryptPassword(string cipherText)
141+
{
142+
if (string.IsNullOrEmpty(cipherText))
143+
return string.Empty;
144+
145+
try
146+
{
147+
var machineKey = GenerateMachineSpecificKey();
148+
var cipherBytes = Convert.FromBase64String(cipherText);
149+
150+
// Extract salt, IV, and encrypted data
151+
var saltSize = SaltSize / 8;
152+
var ivSize = IvSize / 8;
153+
154+
if (cipherBytes.Length < saltSize + ivSize)
155+
{
156+
throw new ArgumentException("Invalid cipher text format");
157+
}
158+
159+
var salt = new byte[saltSize];
160+
var iv = new byte[ivSize];
161+
var encryptedData = new byte[cipherBytes.Length - saltSize - ivSize];
162+
163+
Array.Copy(cipherBytes, 0, salt, 0, saltSize);
164+
Array.Copy(cipherBytes, saltSize, iv, 0, ivSize);
165+
Array.Copy(cipherBytes, saltSize + ivSize, encryptedData, 0, encryptedData.Length);
166+
167+
// Derive key from machine-specific data and extracted salt
168+
using (var pbkdf2 = new Rfc2898DeriveBytes(machineKey, salt, DerivationIterations))
169+
{
170+
var keyBytes = pbkdf2.GetBytes(KeySize / 8);
171+
172+
using (var aes = Aes.Create())
173+
{
174+
aes.KeySize = KeySize;
175+
aes.BlockSize = IvSize;
176+
aes.Mode = CipherMode.CBC;
177+
aes.Padding = PaddingMode.PKCS7;
178+
aes.Key = keyBytes;
179+
aes.IV = iv;
180+
181+
using (var decryptor = aes.CreateDecryptor())
182+
using (var msDecrypt = new MemoryStream(encryptedData))
183+
using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
184+
{
185+
var decryptedBytes = new byte[encryptedData.Length];
186+
var bytesRead = csDecrypt.Read(decryptedBytes, 0, decryptedBytes.Length);
187+
return Encoding.UTF8.GetString(decryptedBytes, 0, bytesRead);
188+
}
189+
}
190+
}
191+
}
192+
catch (Exception ex)
193+
{
194+
Message($"Error decrypting password: {ex.Message}", EventType.Error, 3033);
195+
// Return empty string if decryption fails (might be old format or corrupted)
196+
return string.Empty;
197+
}
198+
}
199+
200+
/// <summary>
201+
/// Migrates passwords encrypted with the old StringCipher method to the new secure method
202+
/// </summary>
203+
/// <param name="oldEncryptedPassword">Password encrypted with StringCipher</param>
204+
/// <param name="oldPassPhrase">The passphrase used with StringCipher</param>
205+
/// <returns>Password encrypted with new method, or empty string if migration fails</returns>
206+
public static string MigrateFromStringCipher(string oldEncryptedPassword, string oldPassPhrase)
207+
{
208+
try
209+
{
210+
// Try to decrypt with old StringCipher method
211+
var decryptedPassword = StringCipher.Decrypt(oldEncryptedPassword, oldPassPhrase);
212+
213+
// Re-encrypt with new secure method
214+
var newEncryptedPassword = EncryptPassword(decryptedPassword);
215+
216+
Message("Successfully migrated old encrypted password to new encryption method", EventType.Information, 3034);
217+
return newEncryptedPassword;
218+
}
219+
catch (Exception ex)
220+
{
221+
Message($"Failed to migrate old encrypted password: {ex.Message}", EventType.Warning, 3035);
222+
return string.Empty;
223+
}
224+
}
225+
226+
/// <summary>
227+
/// Generates cryptographically secure random bytes
228+
/// </summary>
229+
/// <param name="size">Number of bytes to generate</param>
230+
/// <returns>Array of random bytes</returns>
231+
private static byte[] GenerateRandomBytes(int size)
232+
{
233+
var bytes = new byte[size];
234+
using (var rng = RandomNumberGenerator.Create())
235+
{
236+
rng.GetBytes(bytes);
237+
}
238+
return bytes;
239+
}
240+
241+
/// <summary>
242+
/// Validates if the current machine can decrypt a given encrypted password
243+
/// </summary>
244+
/// <param name="encryptedPassword">Encrypted password to test</param>
245+
/// <returns>True if the password can be decrypted on this machine</returns>
246+
public static bool CanDecryptOnThisMachine(string encryptedPassword)
247+
{
248+
try
249+
{
250+
var decrypted = DecryptPassword(encryptedPassword);
251+
return !string.IsNullOrEmpty(decrypted);
252+
}
253+
catch
254+
{
255+
return false;
256+
}
257+
}
258+
}
259+
}

src/SignToolGUI/Forms/MainForm.cs

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,8 +1025,46 @@ private void MainForm_Load(object sender, EventArgs e)
10251025
// Set the certificate password from the configuration file if it exists and decrypt it
10261026
if (settingCertificatePasswordEncrypted != "")
10271027
{
1028-
// TODO - Decrypt the certificate password based on hardware hash
1029-
textBoxPFXPassword.Text = StringCipher.Decrypt(settingCertificatePasswordEncrypted, "pMmInS?m24Caae#?2EySvsFUgDsUG06Qzz8R0X8F8WUNn04#g%mP02*36datrZka?cQh/Q2E/Oc4/21%");
1028+
try
1029+
{
1030+
// First try new secure encryption method
1031+
var decryptedPassword = SecurePasswordManager.DecryptPassword(settingCertificatePasswordEncrypted);
1032+
1033+
if (string.IsNullOrEmpty(decryptedPassword))
1034+
{
1035+
// If new method fails, try to migrate from old StringCipher method
1036+
Message("Attempting to migrate password from old encryption method", EventType.Information, 3036);
1037+
var migratedPassword = SecurePasswordManager.MigrateFromStringCipher(
1038+
settingCertificatePasswordEncrypted,
1039+
"pMmInS?m24Caae#?2EySvsFUgDsUG06Qzz8R0X8F8WUNn04#g%mP02*36datrZka?cQh/Q2E/Oc4/21%");
1040+
1041+
if (!string.IsNullOrEmpty(migratedPassword))
1042+
{
1043+
// Save the migrated password immediately
1044+
iniFile.WriteValue("Program", "CertificatePassword", migratedPassword);
1045+
decryptedPassword = SecurePasswordManager.DecryptPassword(migratedPassword);
1046+
Message("Password successfully migrated and saved with new encryption", EventType.Information, 3037);
1047+
}
1048+
}
1049+
1050+
textBoxPFXPassword.Text = decryptedPassword;
1051+
}
1052+
catch (Exception ex)
1053+
{
1054+
Message($"Failed to decrypt certificate password: {ex.Message}", EventType.Error, 3038);
1055+
textBoxPFXPassword.Text = "";
1056+
1057+
// Optionally clear the corrupted password from config
1058+
if (MessageBox.Show("The saved password could not be decrypted. This may be due to " +
1059+
"the password being encrypted on a different machine or corrupted data. " +
1060+
"Would you like to clear the saved password?",
1061+
"Password Decryption Failed",
1062+
MessageBoxButtons.YesNo,
1063+
MessageBoxIcon.Question) == DialogResult.Yes)
1064+
{
1065+
iniFile.WriteValue("Program", "CertificatePassword", "");
1066+
}
1067+
}
10301068
}
10311069
else
10321070
{
@@ -1131,8 +1169,7 @@ private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
11311169
}
11321170

11331171
// Encrypt the certificate password and save it to the configuration file
1134-
var encryptedstring = StringCipher.Encrypt(textBoxPFXPassword.Text,
1135-
"pMmInS?m24Caae#?2EySvsFUgDsUG06Qzz8R0X8F8WUNn04#g%mP02*36datrZka?cQh/Q2E/Oc4/21%");
1172+
var encryptedstring = SecurePasswordManager.EncryptPassword(textBoxPFXPassword.Text);
11361173

11371174
// Save the encrypted certificate password to the configuration file
11381175
iniFile.WriteValue("Program", "CertificatePassword", encryptedstring);

src/SignToolGUI/SignToolGUI.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
<Compile Include="Class\FileLogger.cs" />
9191
<Compile Include="Class\HighDpi.cs" />
9292
<Compile Include="Class\NativeMethods.cs" />
93+
<Compile Include="Class\SecurePasswordManager.cs" />
9394
<Compile Include="Class\SignerBase.cs" />
9495
<Compile Include="Class\SignerTrustedSigning.cs" />
9596
<Compile Include="Class\SignTool.cs" />

0 commit comments

Comments
 (0)