GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
Cryptography.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using System;
18using DocumentFormat.OpenXml.Drawing;
19using DocumentFormat.OpenXml.ExtendedProperties;
20using DocumentFormat.OpenXml.Math;
21using Org.BouncyCastle.Asn1.Pkcs;
22using Org.BouncyCastle.Asn1.X509;
23using Org.BouncyCastle.Crypto;
24using Org.BouncyCastle.Crypto.Engines;
25using Org.BouncyCastle.Crypto.Generators;
26using Org.BouncyCastle.Crypto.Modes;
27using Org.BouncyCastle.Crypto.Paddings;
28using Org.BouncyCastle.Crypto.Parameters;
29using Org.BouncyCastle.Pkcs;
30using Org.BouncyCastle.Security;
31using Org.BouncyCastle.X509;
32using static GenerallyPositive.Enums;
33
34namespace GenerallyPositive
35{
36 public class Cryptography : ICryptography
37 {
38 private CryptoAlgorithm _algorithm = CryptoAlgorithm.AES; // Default
39 private byte[] _key;
40 private byte[] _iv;
41 private CryptoMode _mode = CryptoMode.GCM; // Default
42 private CryptoPadding _padding = CryptoPadding.PKCS7; // Default
43 private byte[] _salt;
44 private int _iterationCount = 10000; // Default for PBKDF2
45 private byte[] _inputData;
46 private byte[] _signature; // Added for verification
47
48 internal Cryptography()
49 { }
50
51 public IAllowCryptoConfig WithAlgorithm(CryptoAlgorithm alg)
52 {
53 _algorithm = alg;
54 return this;
55 }
56
57 public IAllowCryptoConfig WithKey(byte[] key)
58 {
59 _key = key;
60 return this;
61 }
62
63 public IAllowCryptoConfig WithIV(byte[] iv)
64 {
65 _iv = iv;
66 return this;
67 }
68
69 public IAllowCryptoConfig WithMode(CryptoMode mode)
70 {
71 _mode = mode;
72 return this;
73 }
74
75 public IAllowCryptoConfig WithPadding(CryptoPadding padding)
76 {
77 _padding = padding;
78 return this;
79 }
80
87 private IBufferedCipher BuildAesCipher()
88 {
89 IBlockCipher engine = CryptoMode.CBC == _mode
90 ? (IBlockCipher)new CbcBlockCipher(new AesEngine())
91 : new AesEngine(); // ECB
92
93 // unpadded wants a whole number of blocks and says so itself when it does not get them, which is a
94 // better answer than padding data the caller asked not to have padded
95 IBufferedCipher retVal = CryptoPadding.NoPadding == _padding
96 ? new BufferedBlockCipher(engine)
97 : new PaddedBufferedBlockCipher(engine, CryptoPadding.ISO10126 == _padding
98 ? (IBlockCipherPadding)new ISO10126d2Padding()
99 : new Pkcs7Padding());
100
101 return retVal;
102 }
103
104 public IAllowCryptoConfig WithSalt(byte[] salt)
105 {
106 _salt = salt;
107 return this;
108 }
109
110 public IAllowCryptoConfig WithSignature(byte[] signature)
111 {
112 _signature = signature;
113 return this;
114 }
115 public IAllowCryptoConfig WithIterationCount(int count)
116 {
117 if (count <= 0)
118 {
119 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Iteration count [{count}] is invalid, using default [{_iterationCount}]", this, GPALObjectType.Cryptography);
120 return this;
121 }
122 _iterationCount = count;
123 return this;
124 }
125
126 public IAllowCryptoAction WithInputData(byte[] data)
127 {
128 _inputData = data;
129 return this;
130 }
131
132 public IAllowCryptoConfig GenerateIV(out byte[] iv)
133 {
134 iv = null;
135 if (_algorithm != CryptoAlgorithm.AES)
136 {
137 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Algorithm [{_algorithm}] not supported for IV generation, defaulting to AES", this, GPALObjectType.Cryptography);
138 _algorithm = CryptoAlgorithm.AES;
139 }
140 if (_mode == CryptoMode.ECB)
141 {
142 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "IV generation not required for ECB mode", this, GPALObjectType.Cryptography);
143 return this;
144 }
145
146 GPAL.PublishSimpleEvent(GPALEventType.INFO,
147 $"Generating IV for algorithm [{_algorithm}], mode [{_mode}]",
148 this, GPALObjectType.Cryptography);
149
150 try
151 {
152 int ivSize = _mode == CryptoMode.GCM ? 12 : 16; // 12 bytes for GCM, 16 for CBC
153 iv = new byte[ivSize];
154 var random = new SecureRandom();
155 random.NextBytes(iv);
156 }
157 catch (Exception ex)
158 {
159 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, "IV generation failed", this, GPALObjectType.Cryptography, ex);
160 iv = null;
161 }
162
163 _iv = iv; // Store for state reuse
164 return this;
165 }
166
167 public IAllowCryptoConfig Encrypt(out byte[] encryptedData)
168 {
169 encryptedData = null;
170 if (_key == null)
171 {
172 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Cannot encrypt: missing key", this, GPALObjectType.Cryptography);
173 return this;
174 }
175 if (_inputData == null)
176 {
177 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Cannot encrypt: missing input data", this, GPALObjectType.Cryptography);
178 return this;
179 }
180 if (_mode != CryptoMode.ECB && _iv == null)
181 {
182 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"IV is required for mode [{_mode}], defaulting to ECB", this, GPALObjectType.Cryptography);
183 _mode = CryptoMode.ECB;
184 }
185
186 GPAL.PublishSimpleEvent(GPALEventType.INFO,
187 $"Encrypting with algorithm [{_algorithm}], mode [{_mode}], padding [{_padding}]",
188 this, GPALObjectType.Cryptography);
189
190 try
191 {
192 encryptedData = PerformEncryption();
193 }
194 catch (Exception ex)
195 {
196 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, "Encryption failed", this, GPALObjectType.Cryptography, ex);
197 encryptedData = null;
198 }
199 return this;
200 }
201
202 public IAllowCryptoConfig Decrypt(out string decryptedValue)
203 {
204 decryptedValue = null;
205 if (_key == null)
206 {
207 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Cannot decrypt: missing key", this, GPALObjectType.Cryptography);
208 return this;
209 }
210 if (_inputData == null)
211 {
212 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Cannot decrypt: missing input data", this, GPALObjectType.Cryptography);
213 return this;
214 }
215 if (_mode != CryptoMode.ECB && _iv == null)
216 {
217 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"IV is required for mode [{_mode}], defaulting to ECB", this, GPALObjectType.Cryptography);
218 _mode = CryptoMode.ECB;
219 }
220
221 GPAL.PublishSimpleEvent(GPALEventType.INFO,
222 $"Decrypting with algorithm [{_algorithm}], mode [{_mode}], padding [{_padding}]",
223 this, GPALObjectType.Cryptography);
224
225 try
226 {
227 byte[] decryptedBytes = PerformDecryption();
228 decryptedValue = System.Text.Encoding.UTF8.GetString(decryptedBytes);
229 }
230 catch (Exception ex)
231 {
232 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, "Decryption failed", this, GPALObjectType.Cryptography, ex);
233 decryptedValue = null;
234 }
235 return this;
236 }
237
238 public IAllowCryptoConfig Sign(out byte[] signature)
239 {
240 signature = null;
241 if (_key == null)
242 {
243 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Cannot sign: missing key", this, GPALObjectType.Cryptography);
244 return this;
245 }
246 if (_inputData == null)
247 {
248 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Cannot sign: missing input data", this, GPALObjectType.Cryptography);
249 return this;
250 }
251
252 GPAL.PublishSimpleEvent(GPALEventType.INFO,
253 $"Signing with algorithm [{_algorithm}]",
254 this, GPALObjectType.Cryptography);
255
256 try
257 {
258 signature = PerformSigning();
259 }
260 catch (Exception ex)
261 {
262 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, "Signing failed", this, GPALObjectType.Cryptography, ex);
263 signature = null;
264 }
265 return this;
266 }
267
268 public IAllowCryptoConfig Verify(out bool isValid)
269 {
270 isValid = false;
271 if (_key == null)
272 {
273 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Cannot verify: missing key", this, GPALObjectType.Cryptography);
274 return this;
275 }
276 if (_inputData == null)
277 {
278 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Cannot verify: missing input data", this, GPALObjectType.Cryptography);
279 return this;
280 }
281 if (_signature == null)
282 {
283 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Cannot verify: missing signature", this, GPALObjectType.Cryptography);
284 return this;
285 }
286
287 GPAL.PublishSimpleEvent(GPALEventType.INFO,
288 $"Verifying with algorithm [{_algorithm}]",
289 this, GPALObjectType.Cryptography);
290
291 try
292 {
293 isValid = PerformVerification();
294 }
295 catch (Exception ex)
296 {
297 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, "Verification failed", this, GPALObjectType.Cryptography, ex);
298 isValid = false;
299 }
300 return this;
301 }
302
303 public IAllowCryptoConfig Hash(out byte[] hash)
304 {
305 hash = null;
306 if (_inputData == null)
307 {
308 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Cannot hash: missing input data", this, GPALObjectType.Cryptography);
309 return this;
310 }
311
312 GPAL.PublishSimpleEvent(GPALEventType.INFO,
313 $"Hashing with algorithm [{_algorithm}]" + (_salt != null ? $", salt length [{_salt.Length}]" : ""),
314 this, GPALObjectType.Cryptography);
315
316 try
317 {
318 hash = PerformHashing();
319 }
320 catch (Exception ex)
321 {
322 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, "Hashing failed", this, GPALObjectType.Cryptography, ex);
323 hash = null;
324 }
325 return this;
326 }
327
328 public IAllowCryptoConfig GenerateKey(out byte[] key)
329 {
330 key = null;
331 if (_algorithm == CryptoAlgorithm.PBKDF2 && (_salt == null || _inputData == null))
332 {
333 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Cannot generate key with PBKDF2: missing salt or input data", this, GPALObjectType.Cryptography);
334 return this;
335 }
336
337 GPAL.PublishSimpleEvent(GPALEventType.INFO,
338 $"Generating key with algorithm [{_algorithm}]" + (_algorithm == CryptoAlgorithm.PBKDF2 ? $", iteration count [{_iterationCount}]" : ""),
339 this, GPALObjectType.Cryptography);
340
341 try
342 {
343 key = PerformKeyGeneration();
344 }
345 catch (Exception ex)
346 {
347 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, "Key generation failed", this, GPALObjectType.Cryptography, ex);
348 key = null;
349 }
350 _key = key;
351 return this;
352 }
353
354 private byte[] PerformEncryption()
355 {
356 if (CryptoAlgorithm.AES != _algorithm && CryptoAlgorithm.RSA != _algorithm)
357 {
358 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Algorithm [{_algorithm}] does not encrypt, using AES", this, GPALObjectType.Cryptography);
359 _algorithm = CryptoAlgorithm.AES;
360 }
361
362 // rsa encrypts with the public key and is bounded by the key size, so it is for a small secret such
363 // as an aes key rather than for a file. mode and padding are aes settings and do not apply
364 if (CryptoAlgorithm.RSA == _algorithm)
365 return RsaCipher(true);
366
367 if (_mode == CryptoMode.GCM)
368 {
369 IAeadBlockCipher cipher = new GcmBlockCipher(new AesEngine());
370 ICipherParameters parameters = new AeadParameters(new KeyParameter(_key), 128, _iv, null);
371 cipher.Init(true, parameters);
372 byte[] output = new byte[cipher.GetOutputSize(_inputData.Length)];
373 int len = cipher.ProcessBytes(_inputData, 0, _inputData.Length, output, 0);
374 cipher.DoFinal(output, len);
375 return output;
376 }
377 else
378 {
379 IBufferedCipher cipher = BuildAesCipher();
380
381 ICipherParameters parameters;
382 if (_iv != null)
383 {
384 parameters = new ParametersWithIV(new KeyParameter(_key), _iv);
385 }
386 else
387 {
388 parameters = new KeyParameter(_key);
389 }
390
391 cipher.Init(true, parameters);
392 return cipher.DoFinal(_inputData);
393 }
394 }
395
396 private byte[] PerformDecryption()
397 {
398 if (CryptoAlgorithm.AES != _algorithm && CryptoAlgorithm.RSA != _algorithm)
399 {
400 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Algorithm [{_algorithm}] does not decrypt, using AES", this, GPALObjectType.Cryptography);
401 _algorithm = CryptoAlgorithm.AES;
402 }
403
404 // rsa decrypts with the private key
405 if (CryptoAlgorithm.RSA == _algorithm)
406 return RsaCipher(false);
407
408 if (_mode == CryptoMode.GCM)
409 {
410 IAeadBlockCipher cipher = new GcmBlockCipher(new AesEngine());
411 ICipherParameters parameters = new AeadParameters(new KeyParameter(_key), 128, _iv, null);
412 cipher.Init(false, parameters);
413 byte[] output = new byte[cipher.GetOutputSize(_inputData.Length)];
414 int len = cipher.ProcessBytes(_inputData, 0, _inputData.Length, output, 0);
415 cipher.DoFinal(output, len);
416 return output;
417 }
418 else
419 {
420 IBufferedCipher cipher = BuildAesCipher();
421
422 ICipherParameters parameters;
423 if (_iv != null)
424 {
425 parameters = new ParametersWithIV(new KeyParameter(_key), _iv);
426 }
427 else
428 {
429 parameters = new KeyParameter(_key);
430 }
431
432 cipher.Init(false, parameters);
433 return cipher.DoFinal(_inputData);
434 }
435 }
436
437 private byte[] PerformSigning()
438 {
439 if (CryptoAlgorithm.SHA256withRSA != _algorithm && CryptoAlgorithm.ECDSA != _algorithm)
440 {
441 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Algorithm [{_algorithm}] does not sign, using SHA256withRSA", this, GPALObjectType.Cryptography);
442 _algorithm = CryptoAlgorithm.SHA256withRSA;
443 }
444
445 AsymmetricKeyParameter keyParam;
446 try
447 {
448 // the factory reads the key's own encoding, so an rsa key and an ec key are both handled here
449 // rather than the modulus and exponent being pulled out by hand for one of them
450 keyParam = PrivateKeyFactory.CreateKey(_key);
451 }
452 catch (Exception ex)
453 {
454 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"[{_algorithm}] private key could not be read. It has to be the PKCS#8 encoding of a key for that algorithm", this, GPALObjectType.Cryptography, ex);
455 return null;
456 }
457
458 ISigner signer = SignerUtilities.GetSigner(SignerName());
459 signer.Init(true, keyParam);
460 signer.BlockUpdate(_inputData, 0, _inputData.Length);
461 return signer.GenerateSignature();
462 }
463
464 private bool PerformVerification()
465 {
466 if (CryptoAlgorithm.SHA256withRSA != _algorithm && CryptoAlgorithm.ECDSA != _algorithm)
467 {
468 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Algorithm [{_algorithm}] does not verify, using SHA256withRSA", this, GPALObjectType.Cryptography);
469 _algorithm = CryptoAlgorithm.SHA256withRSA;
470 }
471
472 AsymmetricKeyParameter keyParam;
473 try
474 {
475 keyParam = PublicKeyFactory.CreateKey(_key);
476 }
477 catch (Exception ex)
478 {
479 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"[{_algorithm}] public key could not be read. It has to be the X.509 SubjectPublicKeyInfo encoding of a key for that algorithm", this, GPALObjectType.Cryptography, ex);
480 return false;
481 }
482
483 ISigner signer = SignerUtilities.GetSigner(SignerName());
484 signer.Init(false, keyParam);
485 signer.BlockUpdate(_inputData, 0, _inputData.Length);
486 return signer.VerifySignature(_signature); // Assumes _inputData is the signature
487 }
488
489 private byte[] PerformHashing()
490 {
491 if (CryptoAlgorithm.SHA256 != _algorithm && CryptoAlgorithm.MD5 != _algorithm)
492 {
493 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Algorithm [{_algorithm}] does not hash, using SHA256", this, GPALObjectType.Cryptography);
494 _algorithm = CryptoAlgorithm.SHA256;
495 }
496
497 IDigest digest = DigestUtilities.GetDigest(CryptoAlgorithm.MD5 == _algorithm ? "MD5" : "SHA256");
498 if (_salt != null)
499 {
500 digest.BlockUpdate(_salt, 0, _salt.Length);
501 }
502 digest.BlockUpdate(_inputData, 0, _inputData.Length);
503 byte[] hash = new byte[digest.GetDigestSize()];
504 digest.DoFinal(hash, 0);
505 return hash;
506 }
507
508 private byte[] PerformKeyGeneration()
509 {
510 if (_algorithm == CryptoAlgorithm.PBKDF2)
511 {
512 if (_salt == null || _inputData == null)
513 {
514 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Cannot generate key with PBKDF2: missing salt or input data", this, GPALObjectType.Cryptography);
515 return null;
516 }
517
518 Pkcs5S2ParametersGenerator gen = new Pkcs5S2ParametersGenerator();
519 gen.Init(_inputData, _salt, _iterationCount);
520 return ((KeyParameter)gen.GenerateDerivedParameters("AES", 256)).GetKey();
521 }
522
523 if (_algorithm != CryptoAlgorithm.AES)
524 {
525 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Algorithm [{_algorithm}] not supported, defaulting to AES", this, GPALObjectType.Cryptography);
526 _algorithm = CryptoAlgorithm.AES;
527 }
528
529 var generator = GeneratorUtilities.GetKeyGenerator("AES");
530 return generator.GenerateKey();
531 }
536 private string SignerName()
537 {
538 return CryptoAlgorithm.ECDSA == _algorithm ? "SHA256withECDSA" : "SHA256withRSA";
539 }
540
548 private byte[] RsaCipher(bool encrypting)
549 {
550 AsymmetricKeyParameter keyParam;
551
552 try
553 {
554 keyParam = true == encrypting ? PublicKeyFactory.CreateKey(_key) : PrivateKeyFactory.CreateKey(_key);
555 }
556 catch (Exception ex)
557 {
558 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"RSA {(true == encrypting ? "public" : "private")} key could not be read", this, GPALObjectType.Cryptography, ex);
559 return null;
560 }
561
562 IBufferedCipher cipher = CipherUtilities.GetCipher("RSA/NONE/PKCS1Padding");
563 cipher.Init(encrypting, keyParam);
564
565 return cipher.DoFinal(_inputData);
566 }
567
568 #region Helpers
575 public static (byte[], byte[]) GenerateEcdsaKeyPair()
576 {
577 try
578 {
579 var keyPairGen = GeneratorUtilities.GetKeyPairGenerator("ECDSA");
580 keyPairGen.Init(new Org.BouncyCastle.Crypto.Parameters.ECKeyGenerationParameters(
581 Org.BouncyCastle.Asn1.X9.X962NamedCurves.GetOid("prime256v1"), new SecureRandom()));
582
583 var keyPair = keyPairGen.GenerateKeyPair();
584
585 byte[] privateKey = PrivateKeyInfoFactory.CreatePrivateKeyInfo(keyPair.Private).ToAsn1Object().GetDerEncoded();
586 byte[] publicKey = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(keyPair.Public).ToAsn1Object().GetDerEncoded();
587
588 return (privateKey, publicKey);
589 }
590 catch (Exception ex)
591 {
592 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, "Could not generate an ECDSA key pair", null, GPALObjectType.Cryptography, ex);
593 return (null, null);
594 }
595 }
596
597 public static (byte[], byte[]) GenerateRsaKeyPair()
598 {
599 try
600 {
601 var keyPairGen = GeneratorUtilities.GetKeyPairGenerator("RSA");
602 keyPairGen.Init(new KeyGenerationParameters(new SecureRandom(), 2048));
603 var keyPair = keyPairGen.GenerateKeyPair();
604
605 // Private key (PKCS#8)
606 var privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(keyPair.Private);
607 byte[] privateKey = privateKeyInfo.ToAsn1Object().GetDerEncoded();
608
609 // Public key (X.509)
610 var publicKeyInfo = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(keyPair.Public);
611 byte[] publicKey = publicKeyInfo.ToAsn1Object().GetDerEncoded();
612
613 return (privateKey, publicKey);
614 }
615 catch (Exception)
616 {
617 // Since not part of fluent interface, return default values instead of throwing
618 return (null, null);
619 }
620 }
621 #endregion Helpers
622 public ICryptography ToGPALObject()
623 {
624 return this;
625 }
626 }
627}
Everything starts here, all the GPAL controls and global settings using fluent syntax are here....
Definition GPAL.cs:49
static void PublishSimpleEvent(GPALEventType gPALEventType, string msg, dynamic gPALObject=null, Enums.GPALObjectType gPALObjectType=GPALObjectType.None, Exception ex=null)
Publish a message to either the information channel or exception channel (if exception passed in) Pub...
Definition GPAL.cs:2568