From e4e4d0c53f26bfc3b52780c93d3a7ab4df1ec034 Mon Sep 17 00:00:00 2001 From: nd Date: Sun, 15 Oct 2023 12:57:07 +0200 Subject: [PATCH] Dev (#22) * Changed PQ automatic configuration to use the implemented algorithms as default, moving the classical non-PQ algorithms as counter algorithms, or check the hybrid algorithms in strict mode, too + Added `RandomDataProvider` + Added `Extensions` which contains methods for getting/restoring a `VmpcRandomGenerator` internal state + Added `ChaCha20Rng` + Added `CipherRng` + Added `XSalsa20Rng` + Added `BouncyCastleRngWrapper` to merge the `wan24-Crypto` and Bouncy Castle RNG APIs by wrapping Bouncy Castle's `IRandomGenerator` with `ISeedableRng` + Added `DisposableRngWrapper` + Added `IBouncyCastleRng` + Added NTRUEncrypt asymmetric algorithm (but disabled 'cause of a bug in the Bouncy Castle library) + `BouncyCastleRandomGenerator` does forward seed to `RND` now - Fixed asymmetric key disposing --- README.md | 84 +++- .../wan24-Crypto-BC Tests.csproj | 2 +- .../AsymmetricDilithiumPrivateKey.cs | 15 + .../AsymmetricFalconPrivateKey.cs | 9 + .../AsymmetricFrodoKemPrivateKey.cs | 9 + .../AsymmetricKyberPrivateKey.cs | 9 + .../AsymmetricNtruEncryptAlgorithm.cs | 71 ++++ .../AsymmetricNtruEncryptPrivateKey.cs | 66 +++ .../AsymmetricNtruEncryptPublicKey.cs | 55 +++ src/wan24-Crypto-BC/AsymmetricNtruHelper.cs | 38 ++ .../AsymmetricSphincsPlusPrivateKey.cs | 9 + src/wan24-Crypto-BC/Bootstrap.cs | 36 +- src/wan24-Crypto-BC/BouncyCastle.cs | 7 +- .../BouncyCastleAsymmetricPrivateKeyBase.cs | 4 +- .../BouncyCastleRandomGenerator.cs | 40 +- src/wan24-Crypto-BC/BouncyCastleRngWrapper.cs | 80 ++++ src/wan24-Crypto-BC/ChaCha20Rng.cs | 18 + src/wan24-Crypto-BC/DisposableRngWrapper.cs | 70 ++++ src/wan24-Crypto-BC/Extensions.cs | 52 +++ src/wan24-Crypto-BC/IBouncyCastleRng.cs | 11 + src/wan24-Crypto-BC/README.md | 84 +++- src/wan24-Crypto-BC/RandomDataProvider.cs | 395 ++++++++++++++++++ src/wan24-Crypto-BC/StreamCipherRng.cs | 196 +++++++++ src/wan24-Crypto-BC/XSalsa20Rng.cs | 20 + src/wan24-Crypto-BC/wan24-Crypto-BC.csproj | 4 +- 25 files changed, 1353 insertions(+), 31 deletions(-) create mode 100644 src/wan24-Crypto-BC/AsymmetricNtruEncryptAlgorithm.cs create mode 100644 src/wan24-Crypto-BC/AsymmetricNtruEncryptPrivateKey.cs create mode 100644 src/wan24-Crypto-BC/AsymmetricNtruEncryptPublicKey.cs create mode 100644 src/wan24-Crypto-BC/AsymmetricNtruHelper.cs create mode 100644 src/wan24-Crypto-BC/BouncyCastleRngWrapper.cs create mode 100644 src/wan24-Crypto-BC/ChaCha20Rng.cs create mode 100644 src/wan24-Crypto-BC/DisposableRngWrapper.cs create mode 100644 src/wan24-Crypto-BC/Extensions.cs create mode 100644 src/wan24-Crypto-BC/IBouncyCastleRng.cs create mode 100644 src/wan24-Crypto-BC/RandomDataProvider.cs create mode 100644 src/wan24-Crypto-BC/StreamCipherRng.cs create mode 100644 src/wan24-Crypto-BC/XSalsa20Rng.cs diff --git a/README.md b/README.md index 8aab545..0f693c3 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ the `wan24-Crypto` library with these algorithms: | CRYSTALS-Dilithium | 3 | CRYSTALSDILITHIUM | | FALCON | 4 | FALCON | | SPHINCS+ | 5 | SPHINCSPLUS | -| FrodoKEM | 6 | FRODOKEM | +| FrodoKEM* | 6 | FRODOKEM | +| NTRUEncrypt* | 7 | NTRUENCRYPT | | **Symmetric** | | | | ChaCha20 | 1 | CHACHA20 | | XSalsa20 | 2 | XSALSA20 | @@ -30,8 +31,13 @@ the `wan24-Crypto` library with these algorithms: | HMAC-SHA3-384 | 5 | HMAC-SHA3-384 | | HMAC-SHA3-512 | 6 | HMAC-SHA3-512 | -**NOTE**: FrodoKEM is currently disabled, 'cause there seems to be a bug -(missing code) in the Bouncy Castle library for FrodoKEM. +**NOTE**: FrodoKEM and NTRUEncrypt are currently disabled, 'cause there seems +to be a bug (missing code) in the Bouncy Castle library for +exporting/importing private keys (at last). + +NTRUSign is currently not implemented, 'cause it'd require the using code to +be GPL licensed. This algorithm may be included in a separate package which is +licensed using the GPL license (to avoid misunderstandings) in the future. ## How to get it @@ -81,16 +87,15 @@ These algorithms are designed for post quantum cryptography: - SPHINCS+ (signature) - FrodoKEM (key exchange) -Normally you want to use them in hybrid mode as counter algorithm for -extending a default algorithm of the `wan24-Crypto` package. To do this per -default: +Normally you want to use them in hybrid mode and use classical algorithms of +the `wan24-Crypto` package as counter algorithm. To do this per default: ```cs -// Enable the post quantum algorithms as counter-defaults +// Enable the post quantum algorithms as (counter-)defaults CryptoHelper.ForcePostQuantumSafety(); ``` -This will use these algorithms as counter algorithms for asymmetric +This will use these algorithms as (counter) algorithms for asymmetric cryptography, in case you didn't define other post quantum algorithms already: - CRYSTALS-Kyber (key exchange) @@ -141,3 +146,66 @@ SignatureContainer signature = dataToSign.Sign(yourNormalPrivateKey, options: op For CRYSTALS-Kyber and CRYSTALS-Dilithium the AES parameters are being used. When using SPHINCS+, the Haraka F hashing parameters will be used. For FrodoKEM the AES parameters will be used. + +## Random data provider + +The `RandomDataProvider` is a `RandomDataGenerator` which provides added seed +data to `OnSeed(Async)` attached event handlers. It uses the `ChaCha20Rng` in +combination with `RND` of `wan24-Crypto` to produce cryptographic secure +random data (CSRNG). An instance may be set as `RND.Generator` singleton +random data generator for all consumers (like key generators etc.). + +`RandomDataProvider` can be customized by extending the type. Pregnant methods +are virtual and can be overridden. Since the type is a `HostedServiceBase`, it +can be used in modern .NET app environments. And since it implements the +`IRandomGenerator` interface of Bouncy Castle, it can be used as secure random +data source for all Bouncy Castle algorithms (like key generators) also. + +By calling the `CreateFork(Async)` method, you can create an attached +instance, which will be initialized with a random seed generated by the parent +instance and consumes the provided seeds from the parent automatically. + +**NOTE**: Don't forget to dispose an unused `RandomDataProvider` instance! + +**CAUTION**: There is a patent (US10402172B1) which comes into play, if you +plan to create a Random or Entropy as a Service (R/EaaS) application, +especially when using QRNG entropy. Read that document carefully to avoid +disappointments. + +## Stream cipher RNG + +The `StreamCipherRng` uses any stream cipher to encrypt the generated random +bytes of an underlaying PRNG using a random key. The result is a CSRNG. These +stream ciphers are available with `wan24-Crypto-BC`, but you could use any +other stream cipher (but not AEAD implementations!) also: + +- ChaCha20 - `ChaCha20Rng` +- XSalsa20 - `XSalsa20Rng` + +If you didn't specify an underlaying PRNG, Bouncy Castle's +`VmpcRandomGenerator` will be used and seeded using 256 bytes from `RND`. + +The final CSRNG implements `IRandomGenerator` for use with Bouncy Castle, and +also `ISeedableRng` for use with `RND` (as seed consumer, for example). + +**NOTE**: A `StreamCipherRng` needs to be disposed after use! + +You can use the resulting CSRNG as default RNG for `RND`: + +```cs +ChaCha20Rng csrng = new(); + +// Enable automatic seeding +RND.SeedConsumer = csrng; + +// Use as default CSRNG +RND.FillBytes = csrng.GetBytes; +RND.FillBytesAsync = csrng.GetBytesAsync; +``` + +**NOTE**: When setting the `RND.FillBytes(Async)` callbacks, they may not be +used, if `/dev/urandom` was preferred. To disable `/dev/urandom`, set +`RND.UseDevUrandom` and `RND.RequireDevUrandom` to `false` also. + +**NOTE**: Currently only stream ciphers are supported, because the cipher RNG +implementation doesn't buffer pre-generated random data. diff --git a/src/wan24-Crypto-BC Tests/wan24-Crypto-BC Tests.csproj b/src/wan24-Crypto-BC Tests/wan24-Crypto-BC Tests.csproj index aa23cc4..824c567 100644 --- a/src/wan24-Crypto-BC Tests/wan24-Crypto-BC Tests.csproj +++ b/src/wan24-Crypto-BC Tests/wan24-Crypto-BC Tests.csproj @@ -21,7 +21,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/wan24-Crypto-BC/AsymmetricDilithiumPrivateKey.cs b/src/wan24-Crypto-BC/AsymmetricDilithiumPrivateKey.cs index c3baff4..0ddc60e 100644 --- a/src/wan24-Crypto-BC/AsymmetricDilithiumPrivateKey.cs +++ b/src/wan24-Crypto-BC/AsymmetricDilithiumPrivateKey.cs @@ -49,6 +49,21 @@ protected override void Dispose(bool disposing) privateKey.Tr.Clear(); } + /// + protected override async Task DisposeCore() + { + await base.DisposeCore().DynamicContext(); + if (Keys == null) return; + DilithiumPrivateKeyParameters privateKey = (DilithiumPrivateKeyParameters)Keys.Private; + privateKey.K.Clear(); + privateKey.Rho.Clear(); + privateKey.S1.Clear(); + privateKey.S2.Clear(); + privateKey.T0.Clear(); + privateKey.T1.Clear(); + privateKey.Tr.Clear(); + } + /// /// Cast to public key /// diff --git a/src/wan24-Crypto-BC/AsymmetricFalconPrivateKey.cs b/src/wan24-Crypto-BC/AsymmetricFalconPrivateKey.cs index 3b78e30..b95dbda 100644 --- a/src/wan24-Crypto-BC/AsymmetricFalconPrivateKey.cs +++ b/src/wan24-Crypto-BC/AsymmetricFalconPrivateKey.cs @@ -1,5 +1,6 @@ using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Pqc.Crypto.Falcon; +using wan24.Core; namespace wan24.Crypto.BC { @@ -41,6 +42,14 @@ protected override void Dispose(bool disposing) Keys.Private.ClearPrivateByteArrayFields();//TODO All parameter fields are private :( } + /// + protected override async Task DisposeCore() + { + await base.DisposeCore().DynamicContext(); + if (Keys == null) return; + Keys.Private.ClearPrivateByteArrayFields();//TODO All parameter fields are private :( + } + /// /// Cast to public key /// diff --git a/src/wan24-Crypto-BC/AsymmetricFrodoKemPrivateKey.cs b/src/wan24-Crypto-BC/AsymmetricFrodoKemPrivateKey.cs index 2df80e7..cb153cf 100644 --- a/src/wan24-Crypto-BC/AsymmetricFrodoKemPrivateKey.cs +++ b/src/wan24-Crypto-BC/AsymmetricFrodoKemPrivateKey.cs @@ -1,5 +1,6 @@ using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Pqc.Crypto.Frodo; +using wan24.Core; namespace wan24.Crypto.BC { @@ -42,6 +43,14 @@ protected override void Dispose(bool disposing) Keys.Private.ClearPrivateByteArrayFields();//TODO All parameter fields are private :( } + /// + protected override async Task DisposeCore() + { + await base.DisposeCore().DynamicContext(); + if (Keys == null) return; + Keys.Private.ClearPrivateByteArrayFields();//TODO All parameter fields are private :( + } + /// /// Cast to public key /// diff --git a/src/wan24-Crypto-BC/AsymmetricKyberPrivateKey.cs b/src/wan24-Crypto-BC/AsymmetricKyberPrivateKey.cs index d841149..4dd75bd 100644 --- a/src/wan24-Crypto-BC/AsymmetricKyberPrivateKey.cs +++ b/src/wan24-Crypto-BC/AsymmetricKyberPrivateKey.cs @@ -1,5 +1,6 @@ using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Pqc.Crypto.Crystals.Kyber; +using wan24.Core; namespace wan24.Crypto.BC { @@ -42,6 +43,14 @@ protected override void Dispose(bool disposing) Keys.Private.ClearPrivateByteArrayFields();//TODO All parameter fields are private :( } + /// + protected override async Task DisposeCore() + { + await base.DisposeCore().DynamicContext(); + if (Keys == null) return; + Keys.Private.ClearPrivateByteArrayFields();//TODO All parameter fields are private :( + } + /// /// Cast to public key /// diff --git a/src/wan24-Crypto-BC/AsymmetricNtruEncryptAlgorithm.cs b/src/wan24-Crypto-BC/AsymmetricNtruEncryptAlgorithm.cs new file mode 100644 index 0000000..b99d428 --- /dev/null +++ b/src/wan24-Crypto-BC/AsymmetricNtruEncryptAlgorithm.cs @@ -0,0 +1,71 @@ +using Org.BouncyCastle.Pqc.Crypto.Ntru; +using System.Collections.ObjectModel; + +namespace wan24.Crypto.BC +{ + /// + /// NTRUEncrypt asymmetric algorithm + /// + public sealed record class AsymmetricNtruEncryptAlgorithm + : BouncyCastleAsymmetricAlgorithmBase< + AsymmetricNtruEncryptPublicKey, + AsymmetricNtruEncryptPrivateKey, + NtruKeyPairGenerator, + NtruKeyGenerationParameters, + NtruParameters, + NtruPublicKeyParameters, + NtruPrivateKeyParameters, + AsymmetricNtruEncryptAlgorithm + > + { + /// + /// Algorithm name + /// + public const string ALGORITHM_NAME = "NTRUENCRYPT"; + /// + /// Algorithm value + /// + public const int ALGORITHM_VALUE = 7; + /// + /// Default key size in bits + /// + public const int DEFAULT_KEY_SIZE = 701; + /// + /// Algorithm usages + /// + public const AsymmetricAlgorithmUsages USAGES = AsymmetricAlgorithmUsages.KeyExchange; + /// + /// Display name + /// + public const string DISPLAY_NAME = "NTRUEncrypt"; + + /// + /// Allowed key sizes in bits + /// + private static readonly ReadOnlyCollection _AllowedKeySizes; + + /// + /// Static constructor + /// + static AsymmetricNtruEncryptAlgorithm() => _AllowedKeySizes = new List() + { + 509, + 677, + 701, + 821 + }.AsReadOnly(); + + /// + /// Constructor + /// + public AsymmetricNtruEncryptAlgorithm() + : base(ALGORITHM_NAME, ALGORITHM_VALUE, USAGES, isEllipticCurveAlgorithm: false, _AllowedKeySizes, isPostQuantum: true, DEFAULT_KEY_SIZE) + { } + + /// + public override string DisplayName => DISPLAY_NAME; + + /// + protected override NtruParameters GetEngineParameters(CryptoOptions options) => AsymmetricNtruHelper.GetParameters(options.AsymmetricKeyBits); + } +} diff --git a/src/wan24-Crypto-BC/AsymmetricNtruEncryptPrivateKey.cs b/src/wan24-Crypto-BC/AsymmetricNtruEncryptPrivateKey.cs new file mode 100644 index 0000000..944559b --- /dev/null +++ b/src/wan24-Crypto-BC/AsymmetricNtruEncryptPrivateKey.cs @@ -0,0 +1,66 @@ +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Pqc.Crypto.Ntru; +using wan24.Core; + +namespace wan24.Crypto.BC +{ + /// + /// NTRUEncrypt asymmetric private key + /// + public sealed record class AsymmetricNtruEncryptPrivateKey + : BouncyCastleAsymmetricPrivateKeyExchangeKeyBase< + AsymmetricNtruEncryptPublicKey, + AsymmetricNtruEncryptAlgorithm, + NtruPublicKeyParameters, + NtruPrivateKeyParameters, + NtruKemGenerator, + NtruKemExtractor, + AsymmetricNtruEncryptPrivateKey + > + { + /// + /// Constructor + /// + public AsymmetricNtruEncryptPrivateKey() : base(AsymmetricNtruEncryptAlgorithm.ALGORITHM_NAME) { } + + /// + /// Constructor + /// + /// Key data + public AsymmetricNtruEncryptPrivateKey(byte[] keyData) : base(AsymmetricNtruEncryptAlgorithm.ALGORITHM_NAME, keyData) { } + + /// + /// Constructor + /// + /// Keys + public AsymmetricNtruEncryptPrivateKey(AsymmetricCipherKeyPair keys) : base(AsymmetricNtruEncryptAlgorithm.ALGORITHM_NAME, keys) { } + + /// + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (Keys == null) return; + Keys.Private.ClearPrivateByteArrayFields();//TODO All parameter fields are private :( + } + + /// + protected override async Task DisposeCore() + { + await base.DisposeCore().DynamicContext(); + if (Keys == null) return; + Keys.Private.ClearPrivateByteArrayFields();//TODO All parameter fields are private :( + } + + /// + /// Cast to public key + /// + /// Private key + public static implicit operator AsymmetricNtruEncryptPublicKey(AsymmetricNtruEncryptPrivateKey privateKey) => privateKey.PublicKey; + + /// + /// Cast from serialized data + /// + /// Data + public static explicit operator AsymmetricNtruEncryptPrivateKey(byte[] data) => Import(data); + } +} diff --git a/src/wan24-Crypto-BC/AsymmetricNtruEncryptPublicKey.cs b/src/wan24-Crypto-BC/AsymmetricNtruEncryptPublicKey.cs new file mode 100644 index 0000000..b4330f8 --- /dev/null +++ b/src/wan24-Crypto-BC/AsymmetricNtruEncryptPublicKey.cs @@ -0,0 +1,55 @@ +using Org.BouncyCastle.Pqc.Crypto.Ntru; + +namespace wan24.Crypto.BC +{ + /// + /// NTRUEncrypt asymmetric public key + /// + public sealed record class AsymmetricNtruEncryptPublicKey + : BouncyCastleAsymmetricPublicKeyBase + { + /// + /// Constructor + /// + public AsymmetricNtruEncryptPublicKey() : base(AsymmetricNtruEncryptAlgorithm.ALGORITHM_NAME) { } + + /// + /// Constructor + /// + /// Key data + public AsymmetricNtruEncryptPublicKey(byte[] keyData) : base(AsymmetricNtruEncryptAlgorithm.ALGORITHM_NAME, keyData) { } + + /// + /// Constructor + /// + /// Public key + public AsymmetricNtruEncryptPublicKey(NtruPublicKeyParameters publicKey) : base(AsymmetricNtruEncryptAlgorithm.ALGORITHM_NAME, publicKey) { } + + /// + public override int Bits + { + get + { + try + { + EnsureUndisposed(); + return _PublicKey?.Parameters.GetKeySize() ?? throw new InvalidOperationException(); + } + catch (CryptographicException) + { + throw; + } + catch (Exception ex) + { + throw CryptographicException.From(ex); + } + } + } + + /// + /// Cast from serialized data + /// + /// Data + public static explicit operator AsymmetricNtruEncryptPublicKey(byte[] data) => Import(data); + } +} diff --git a/src/wan24-Crypto-BC/AsymmetricNtruHelper.cs b/src/wan24-Crypto-BC/AsymmetricNtruHelper.cs new file mode 100644 index 0000000..8cd3a6d --- /dev/null +++ b/src/wan24-Crypto-BC/AsymmetricNtruHelper.cs @@ -0,0 +1,38 @@ +using Org.BouncyCastle.Pqc.Crypto.Ntru; + +namespace wan24.Crypto.BC +{ + /// + /// NTRU helper + /// + public static class AsymmetricNtruHelper + { + /// + /// Get the key size in bits + /// + /// Parameters + /// Key size in bits + public static int GetKeySize(this NtruParameters param) + { + if (param == NtruParameters.NtruHps2048509) return 509; + if (param == NtruParameters.NtruHps2048677) return 677; + if (param == NtruParameters.NtruHps4096821) return 821; + if (param == NtruParameters.NtruHrss701) return 701; + throw new ArgumentException("Invalid NTRU parameters", nameof(param)); + } + + /// + /// Get the NTRU parameters + /// + /// Key size in bits + /// Parameters + public static NtruParameters GetParameters(int keySize) => keySize switch + { + 509 => NtruParameters.NtruHps2048509, + 677 => NtruParameters.NtruHps2048677, + 821 => NtruParameters.NtruHps4096821, + 701 => NtruParameters.NtruHrss701, + _ => throw new ArgumentException("Invalid key size", nameof(keySize)) + }; + } +} diff --git a/src/wan24-Crypto-BC/AsymmetricSphincsPlusPrivateKey.cs b/src/wan24-Crypto-BC/AsymmetricSphincsPlusPrivateKey.cs index 5c6d033..81d46b5 100644 --- a/src/wan24-Crypto-BC/AsymmetricSphincsPlusPrivateKey.cs +++ b/src/wan24-Crypto-BC/AsymmetricSphincsPlusPrivateKey.cs @@ -1,5 +1,6 @@ using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Pqc.Crypto.SphincsPlus; +using wan24.Core; namespace wan24.Crypto.BC { @@ -41,6 +42,14 @@ protected override void Dispose(bool disposing) Keys.Private.ClearPrivateByteArrayFields();//TODO All parameter fields are private :( } + /// + protected override async Task DisposeCore() + { + await base.DisposeCore().DynamicContext(); + if (Keys == null) return; + Keys.Private.ClearPrivateByteArrayFields();//TODO All parameter fields are private :( + } + /// /// Cast to public key /// diff --git a/src/wan24-Crypto-BC/Bootstrap.cs b/src/wan24-Crypto-BC/Bootstrap.cs index a0a0553..77d05c2 100644 --- a/src/wan24-Crypto-BC/Bootstrap.cs +++ b/src/wan24-Crypto-BC/Bootstrap.cs @@ -21,6 +21,8 @@ public static void Boot() AsymmetricHelper.Algorithms[AsymmetricDilithiumAlgorithm.ALGORITHM_NAME] = AsymmetricDilithiumAlgorithm.Instance; AsymmetricHelper.Algorithms[AsymmetricFalconAlgorithm.ALGORITHM_NAME] = AsymmetricFalconAlgorithm.Instance; AsymmetricHelper.Algorithms[AsymmetricSphincsPlusAlgorithm.ALGORITHM_NAME] = AsymmetricSphincsPlusAlgorithm.Instance; + //FIXME PqcPrivateKeyInfoFactory.CreatePrivateKeyInfo doesn't support NtruPrivateKeyParameters !? (waiting for a fix and an update of the NuGet package at present) + //AsymmetricHelper.Algorithms[AsymmetricNtruEncryptAlgorithm.ALGORITHM_NAME] = AsymmetricNtruEncryptAlgorithm.Instance; // ChaCha20 EncryptionHelper.Algorithms[EncryptionChaCha20Algorithm.ALGORITHM_NAME] = EncryptionChaCha20Algorithm.Instance; CryptoProfiles.Registered[EncryptionChaCha20Algorithm.PROFILE_CHACHA20_RAW] = new CryptoOptions() @@ -71,6 +73,7 @@ public static void Boot() .WithoutMac() .WithEncryptionAlgorithm(EncryptionTwofish256GcmAlgorithm.ALGORITHM_NAME); // Hash + //TODO .NET 8: SHA3 HashHelper.Algorithms[HashSha3_256Algorithm.ALGORITHM_NAME] = HashSha3_256Algorithm.Instance; HashHelper.Algorithms[HashSha3_384Algorithm.ALGORITHM_NAME] = HashSha3_384Algorithm.Instance; HashHelper.Algorithms[HashSha3_512Algorithm.ALGORITHM_NAME] = HashSha3_512Algorithm.Instance; @@ -81,19 +84,48 @@ public static void Boot() // PQ CryptoHelper.OnForcePostQuantum += (e) => { + //TODO Implement NTRU encryption as new default algorithm for key exchange in v2 if (CryptoHelper.StrictPostQuantumSafety) { if (!AsymmetricHelper.DefaultKeyExchangeAlgorithm.IsPostQuantum) + { AsymmetricHelper.DefaultKeyExchangeAlgorithm = AsymmetricKyberAlgorithm.Instance; + } + else if(!(HybridAlgorithmHelper.KeyExchangeAlgorithm?.IsPostQuantum ?? true)) + { + HybridAlgorithmHelper.KeyExchangeAlgorithm = AsymmetricKyberAlgorithm.Instance; + } if (!AsymmetricHelper.DefaultSignatureAlgorithm.IsPostQuantum) + { AsymmetricHelper.DefaultSignatureAlgorithm = AsymmetricDilithiumAlgorithm.Instance; + } + else if (!(HybridAlgorithmHelper.SignatureAlgorithm?.IsPostQuantum ?? true)) + { + HybridAlgorithmHelper.SignatureAlgorithm = AsymmetricDilithiumAlgorithm.Instance; + } } else { - if (!(HybridAlgorithmHelper.KeyExchangeAlgorithm?.IsPostQuantum ?? false)) + if(AsymmetricHelper.DefaultKeyExchangeAlgorithm.IsPostQuantum && !(HybridAlgorithmHelper.KeyExchangeAlgorithm?.IsPostQuantum ?? false)) + { HybridAlgorithmHelper.KeyExchangeAlgorithm = AsymmetricKyberAlgorithm.Instance; - if (!(HybridAlgorithmHelper.SignatureAlgorithm?.IsPostQuantum ?? false)) + } + else if (!AsymmetricHelper.DefaultKeyExchangeAlgorithm.IsPostQuantum) + { + if (!(HybridAlgorithmHelper.KeyExchangeAlgorithm?.IsPostQuantum ?? false)) + HybridAlgorithmHelper.KeyExchangeAlgorithm = AsymmetricHelper.DefaultKeyExchangeAlgorithm; + AsymmetricHelper.DefaultKeyExchangeAlgorithm = AsymmetricKyberAlgorithm.Instance; + } + if (AsymmetricHelper.DefaultSignatureAlgorithm.IsPostQuantum && !(HybridAlgorithmHelper.SignatureAlgorithm?.IsPostQuantum ?? false)) + { HybridAlgorithmHelper.SignatureAlgorithm = AsymmetricDilithiumAlgorithm.Instance; + } + else if (!AsymmetricHelper.DefaultSignatureAlgorithm.IsPostQuantum) + { + if (!(HybridAlgorithmHelper.SignatureAlgorithm?.IsPostQuantum ?? false)) + HybridAlgorithmHelper.SignatureAlgorithm = AsymmetricHelper.DefaultSignatureAlgorithm; + AsymmetricHelper.DefaultSignatureAlgorithm = AsymmetricDilithiumAlgorithm.Instance; + } } }; } diff --git a/src/wan24-Crypto-BC/BouncyCastle.cs b/src/wan24-Crypto-BC/BouncyCastle.cs index 1d40c29..0af5f1b 100644 --- a/src/wan24-Crypto-BC/BouncyCastle.cs +++ b/src/wan24-Crypto-BC/BouncyCastle.cs @@ -1,6 +1,4 @@ -//TODO Implement ECDH, ECDSA to replace wan24-Crypto algoritms - -namespace wan24.Crypto.BC +namespace wan24.Crypto.BC { /// /// Bouncy Castle helper @@ -13,6 +11,7 @@ public static class BouncyCastle /// Use the current wan24-Crypto defaults as counter algorithms? public static void SetDefaults(in bool useCurrentDefaultAsCounterAlgorithms = true) { + //TODO In v2 use NTRU as default asymmetric algorithm for key exchange if (useCurrentDefaultAsCounterAlgorithms) { HybridAlgorithmHelper.KeyExchangeAlgorithm = AsymmetricHelper.DefaultKeyExchangeAlgorithm; @@ -40,7 +39,7 @@ public static void ReplaceNetAlgorithms() EncryptionHelper.Algorithms[EncryptionAes256CbcAlgorithm.ALGORITHM_NAME] = EncryptionBcAes256CbcAlgorithm.Instance; if (EncryptionHelper.DefaultAlgorithm.Value == EncryptionAes256CbcAlgorithm.ALGORITHM_VALUE) EncryptionHelper.DefaultAlgorithm = EncryptionBcAes256CbcAlgorithm.Instance; - //TODO Replace other .NET algorithms + //TODO Implement ECDH, ECDSA to replace wan24-Crypto algoritms } } } diff --git a/src/wan24-Crypto-BC/BouncyCastleAsymmetricPrivateKeyBase.cs b/src/wan24-Crypto-BC/BouncyCastleAsymmetricPrivateKeyBase.cs index e805fb7..2b1b2de 100644 --- a/src/wan24-Crypto-BC/BouncyCastleAsymmetricPrivateKeyBase.cs +++ b/src/wan24-Crypto-BC/BouncyCastleAsymmetricPrivateKeyBase.cs @@ -128,12 +128,12 @@ protected byte[] SerializeKeyData() CleanReturned = true }; ms.WriteNumber(StreamSerializer.VERSION); - byte[] keyInfo = PqcPrivateKeyInfoFactory.CreatePrivateKeyInfo((tPrivateKey)Keys.Private).PrivateKeyData.GetEncoded(); + byte[] keyInfo = PqcPrivateKeyInfoFactory.CreatePrivateKeyInfo(Keys.Private).PrivateKeyData.GetEncoded(); try { ms.WriteBytes(keyInfo); keyInfo.Clear(); - keyInfo = PqcSubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo((tPublicKey)Keys.Public).GetEncoded(); + keyInfo = PqcSubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(Keys.Public).GetEncoded(); ms.WriteBytes(keyInfo); keyInfo.Clear(); } diff --git a/src/wan24-Crypto-BC/BouncyCastleRandomGenerator.cs b/src/wan24-Crypto-BC/BouncyCastleRandomGenerator.cs index 0973b24..ac195b4 100644 --- a/src/wan24-Crypto-BC/BouncyCastleRandomGenerator.cs +++ b/src/wan24-Crypto-BC/BouncyCastleRandomGenerator.cs @@ -1,11 +1,12 @@ using Org.BouncyCastle.Crypto.Prng; +using wan24.Core; namespace wan24.Crypto.BC { /// /// Random number generator for Bouncy Castle, which adopts /// - public sealed class BouncyCastleRandomGenerator : IRandomGenerator + public sealed class BouncyCastleRandomGenerator : IBouncyCastleRng { /// /// Instance @@ -23,13 +24,44 @@ public BouncyCastleRandomGenerator() { } public static Func Instance { get; set; } = () => _Instance; /// - public void AddSeedMaterial(byte[] seed) { } + public void AddSeed(ReadOnlySpan seed) => RND.AddSeed(seed); /// - public void AddSeedMaterial(ReadOnlySpan seed) { } + public Task AddSeedAsync(ReadOnlyMemory seed, CancellationToken cancellationToken = default) => RND.AddSeedAsync(seed, cancellationToken); /// - public void AddSeedMaterial(long seed) { } + public void AddSeedMaterial(byte[] seed) => RND.AddSeed(seed); + + /// + public void AddSeedMaterial(ReadOnlySpan seed) => RND.AddSeed(seed); + + /// + public void AddSeedMaterial(long seed) + { + using RentedArrayRefStruct buffer = new(sizeof(long)); + seed.GetBytes(buffer.Span); + RND.AddSeed(buffer.Span); + } + + /// + public Span FillBytes(in Span buffer) + { + RND.FillBytes(buffer); + return buffer; + } + + /// + public async Task> FillBytesAsync(Memory buffer, CancellationToken cancellationToken = default) + { + await RND.FillBytesAsync(buffer).DynamicContext(); + return buffer; + } + + /// + public byte[] GetBytes(in int count) => RND.GetBytes(count); + + /// + public Task GetBytesAsync(int count, CancellationToken cancellationToken = default) => RND.GetBytesAsync(count); /// public void NextBytes(byte[] bytes) => RND.FillBytes(bytes); diff --git a/src/wan24-Crypto-BC/BouncyCastleRngWrapper.cs b/src/wan24-Crypto-BC/BouncyCastleRngWrapper.cs new file mode 100644 index 0000000..03492e8 --- /dev/null +++ b/src/wan24-Crypto-BC/BouncyCastleRngWrapper.cs @@ -0,0 +1,80 @@ +using Org.BouncyCastle.Crypto.Prng; + +namespace wan24.Crypto.BC +{ + /// + /// Bouncy Castle RNG wrapper for wan24-Crypto + /// + public sealed class BouncyCastleRngWrapper : IBouncyCastleRng + { + /// + /// Constructor + /// + /// RNG + public BouncyCastleRngWrapper(in IRandomGenerator rng) => RNG = rng; + + /// + /// Wrapped RNG + /// + public IRandomGenerator RNG { get; } + + /// + public void AddSeed(ReadOnlySpan seed) => AddSeedMaterial(seed); + + /// + public Task AddSeedAsync(ReadOnlyMemory seed, CancellationToken cancellationToken = default) + { + AddSeedMaterial(seed.Span); + return Task.CompletedTask; + } + + /// + public void AddSeedMaterial(byte[] seed) => RNG.AddSeedMaterial(seed); + + /// + public void AddSeedMaterial(ReadOnlySpan seed) => RNG.AddSeedMaterial(seed); + + /// + public void AddSeedMaterial(long seed) => RNG.AddSeedMaterial(seed); + + /// + public Span FillBytes(in Span buffer) + { + NextBytes(buffer); + return buffer; + } + + /// + public Task> FillBytesAsync(Memory buffer, CancellationToken cancellationToken = default) + { + NextBytes(buffer.Span); + return Task.FromResult(buffer); + } + + /// + public byte[] GetBytes(in int count) + { + if (count < 1) return Array.Empty(); + byte[] res = new byte[count]; + NextBytes(res); + return res; + } + + public Task GetBytesAsync(int count, CancellationToken cancellationToken = default) + { + if (count < 1) return Task.FromResult(Array.Empty()); + byte[] res = new byte[count]; + NextBytes(res); + return Task.FromResult(res); + } + + /// + public void NextBytes(byte[] bytes) => RNG.NextBytes(bytes); + + /// + public void NextBytes(byte[] bytes, int start, int len) => RNG.NextBytes(bytes, start, len); + + /// + public void NextBytes(Span bytes) => RNG.NextBytes(bytes); + } +} diff --git a/src/wan24-Crypto-BC/ChaCha20Rng.cs b/src/wan24-Crypto-BC/ChaCha20Rng.cs new file mode 100644 index 0000000..1eab24d --- /dev/null +++ b/src/wan24-Crypto-BC/ChaCha20Rng.cs @@ -0,0 +1,18 @@ +namespace wan24.Crypto.BC +{ + /// + /// ChaCha20 CSRNG + /// + public sealed class ChaCha20Rng : StreamCipherRng + { + /// + /// Constructor + /// + /// Internal RNG to use (will be disposed, if possible!) + /// Buffer size in bytes (min. ) + /// Seed the given RNG with N byte from (skipped, if or <1) + public ChaCha20Rng(in ISeedableRng? rng = null, in int? bufferSize = null, in int? seedLength = 256) + : base(EncryptionChaCha20Algorithm.Instance, rng, bufferSize, seedLength) + { } + } +} diff --git a/src/wan24-Crypto-BC/DisposableRngWrapper.cs b/src/wan24-Crypto-BC/DisposableRngWrapper.cs new file mode 100644 index 0000000..f29d8c1 --- /dev/null +++ b/src/wan24-Crypto-BC/DisposableRngWrapper.cs @@ -0,0 +1,70 @@ +using Org.BouncyCastle.Crypto.Prng; +using wan24.Core; + +namespace wan24.Crypto.BC +{ + /// + /// Bouncy Castle disposable RNG wrapper for wan24-Crypto + /// + public sealed class DisposableRngWrapper : DisposableSeedableRngBase, IRandomGenerator + { + /// + /// Constructor + /// + /// RNG + public DisposableRngWrapper(IRandomGenerator rng) : base() => RNG = rng; + + /// + /// Wrapped RNG + /// + public IRandomGenerator RNG { get; } + + /// + public override void AddSeed(ReadOnlySpan seed) => AddSeedMaterial(seed); + + /// + public override Task AddSeedAsync(ReadOnlyMemory seed, CancellationToken cancellationToken = default) + { + AddSeedMaterial(seed.Span); + return Task.CompletedTask; + } + + /// + public void AddSeedMaterial(byte[] seed) => RNG.AddSeedMaterial(seed); + + /// + public void AddSeedMaterial(ReadOnlySpan seed) => RNG.AddSeedMaterial(seed); + + /// + public void AddSeedMaterial(long seed) => RNG.AddSeedMaterial(seed); + + /// + public override Span FillBytes(in Span buffer) + { + NextBytes(buffer); + return buffer; + } + + /// + public override Task> FillBytesAsync(Memory buffer, CancellationToken cancellationToken = default) + { + NextBytes(buffer.Span); + return Task.FromResult(buffer); + } + + /// + public void NextBytes(byte[] bytes) => RNG.NextBytes(bytes); + + /// + public void NextBytes(byte[] bytes, int start, int len) => RNG.NextBytes(bytes, start, len); + + /// + public void NextBytes(Span bytes) => RNG.NextBytes(bytes); + + /// + protected override void Dispose(bool disposing) => RNG.TryDispose(); + + /// + protected override async Task DisposeCore() => await RNG.TryDisposeAsync().DynamicContext(); + } +} diff --git a/src/wan24-Crypto-BC/Extensions.cs b/src/wan24-Crypto-BC/Extensions.cs new file mode 100644 index 0000000..b4c90eb --- /dev/null +++ b/src/wan24-Crypto-BC/Extensions.cs @@ -0,0 +1,52 @@ +using Org.BouncyCastle.Crypto.Prng; +using System.Reflection; +using wan24.Core; + +namespace wan24.Crypto.BC +{ + /// + /// Extension methods + /// + public static class Extensions + { + /// + /// type + /// + private static readonly Type VmpcRandomGeneratorType = typeof(VmpcRandomGenerator); + + /// + /// Get the internal state (you shouldn't add seed during this method is being executed!) + /// + /// RNG + /// Internal state + public static byte[] GetState(this VmpcRandomGenerator rng) + { + byte[] p = (byte[])VmpcRandomGeneratorType.GetFieldCached("P", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(rng)!, + res = new byte[p.Length + 2]; + lock (p) + { + p.AsSpan().CopyTo(res); + p[^2] = (byte)VmpcRandomGeneratorType.GetFieldCached("s", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(rng)!; + p[^1] = (byte)VmpcRandomGeneratorType.GetFieldCached("n", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(rng)!; + } + return res; + } + + /// + /// Restore the internal state (you shouldn't add seed during this method is being executed!) + /// + /// RNG + /// Stored internal state + public static void RestoreState(this VmpcRandomGenerator rng, ReadOnlySpan state) + { + byte[] p = (byte[])VmpcRandomGeneratorType.GetFieldCached("P", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(rng)!; + if (state.Length < p.Length + 2) throw new ArgumentOutOfRangeException(nameof(state)); + lock (p) + { + state[..p.Length].CopyTo(p); + VmpcRandomGeneratorType.GetFieldCached("s", BindingFlags.NonPublic | BindingFlags.Instance)!.SetValue(rng, state[p.Length]); + VmpcRandomGeneratorType.GetFieldCached("n", BindingFlags.NonPublic | BindingFlags.Instance)!.SetValue(rng, state[p.Length + 1]); + } + } + } +} diff --git a/src/wan24-Crypto-BC/IBouncyCastleRng.cs b/src/wan24-Crypto-BC/IBouncyCastleRng.cs new file mode 100644 index 0000000..bef45da --- /dev/null +++ b/src/wan24-Crypto-BC/IBouncyCastleRng.cs @@ -0,0 +1,11 @@ +using Org.BouncyCastle.Crypto.Prng; + +namespace wan24.Crypto.BC +{ + /// + /// Interface for a seedable Bouncy Castle supporting RNG + /// + public interface IBouncyCastleRng : IRandomGenerator, ISeedableRng + { + } +} diff --git a/src/wan24-Crypto-BC/README.md b/src/wan24-Crypto-BC/README.md index 8aab545..0f693c3 100644 --- a/src/wan24-Crypto-BC/README.md +++ b/src/wan24-Crypto-BC/README.md @@ -12,7 +12,8 @@ the `wan24-Crypto` library with these algorithms: | CRYSTALS-Dilithium | 3 | CRYSTALSDILITHIUM | | FALCON | 4 | FALCON | | SPHINCS+ | 5 | SPHINCSPLUS | -| FrodoKEM | 6 | FRODOKEM | +| FrodoKEM* | 6 | FRODOKEM | +| NTRUEncrypt* | 7 | NTRUENCRYPT | | **Symmetric** | | | | ChaCha20 | 1 | CHACHA20 | | XSalsa20 | 2 | XSALSA20 | @@ -30,8 +31,13 @@ the `wan24-Crypto` library with these algorithms: | HMAC-SHA3-384 | 5 | HMAC-SHA3-384 | | HMAC-SHA3-512 | 6 | HMAC-SHA3-512 | -**NOTE**: FrodoKEM is currently disabled, 'cause there seems to be a bug -(missing code) in the Bouncy Castle library for FrodoKEM. +**NOTE**: FrodoKEM and NTRUEncrypt are currently disabled, 'cause there seems +to be a bug (missing code) in the Bouncy Castle library for +exporting/importing private keys (at last). + +NTRUSign is currently not implemented, 'cause it'd require the using code to +be GPL licensed. This algorithm may be included in a separate package which is +licensed using the GPL license (to avoid misunderstandings) in the future. ## How to get it @@ -81,16 +87,15 @@ These algorithms are designed for post quantum cryptography: - SPHINCS+ (signature) - FrodoKEM (key exchange) -Normally you want to use them in hybrid mode as counter algorithm for -extending a default algorithm of the `wan24-Crypto` package. To do this per -default: +Normally you want to use them in hybrid mode and use classical algorithms of +the `wan24-Crypto` package as counter algorithm. To do this per default: ```cs -// Enable the post quantum algorithms as counter-defaults +// Enable the post quantum algorithms as (counter-)defaults CryptoHelper.ForcePostQuantumSafety(); ``` -This will use these algorithms as counter algorithms for asymmetric +This will use these algorithms as (counter) algorithms for asymmetric cryptography, in case you didn't define other post quantum algorithms already: - CRYSTALS-Kyber (key exchange) @@ -141,3 +146,66 @@ SignatureContainer signature = dataToSign.Sign(yourNormalPrivateKey, options: op For CRYSTALS-Kyber and CRYSTALS-Dilithium the AES parameters are being used. When using SPHINCS+, the Haraka F hashing parameters will be used. For FrodoKEM the AES parameters will be used. + +## Random data provider + +The `RandomDataProvider` is a `RandomDataGenerator` which provides added seed +data to `OnSeed(Async)` attached event handlers. It uses the `ChaCha20Rng` in +combination with `RND` of `wan24-Crypto` to produce cryptographic secure +random data (CSRNG). An instance may be set as `RND.Generator` singleton +random data generator for all consumers (like key generators etc.). + +`RandomDataProvider` can be customized by extending the type. Pregnant methods +are virtual and can be overridden. Since the type is a `HostedServiceBase`, it +can be used in modern .NET app environments. And since it implements the +`IRandomGenerator` interface of Bouncy Castle, it can be used as secure random +data source for all Bouncy Castle algorithms (like key generators) also. + +By calling the `CreateFork(Async)` method, you can create an attached +instance, which will be initialized with a random seed generated by the parent +instance and consumes the provided seeds from the parent automatically. + +**NOTE**: Don't forget to dispose an unused `RandomDataProvider` instance! + +**CAUTION**: There is a patent (US10402172B1) which comes into play, if you +plan to create a Random or Entropy as a Service (R/EaaS) application, +especially when using QRNG entropy. Read that document carefully to avoid +disappointments. + +## Stream cipher RNG + +The `StreamCipherRng` uses any stream cipher to encrypt the generated random +bytes of an underlaying PRNG using a random key. The result is a CSRNG. These +stream ciphers are available with `wan24-Crypto-BC`, but you could use any +other stream cipher (but not AEAD implementations!) also: + +- ChaCha20 - `ChaCha20Rng` +- XSalsa20 - `XSalsa20Rng` + +If you didn't specify an underlaying PRNG, Bouncy Castle's +`VmpcRandomGenerator` will be used and seeded using 256 bytes from `RND`. + +The final CSRNG implements `IRandomGenerator` for use with Bouncy Castle, and +also `ISeedableRng` for use with `RND` (as seed consumer, for example). + +**NOTE**: A `StreamCipherRng` needs to be disposed after use! + +You can use the resulting CSRNG as default RNG for `RND`: + +```cs +ChaCha20Rng csrng = new(); + +// Enable automatic seeding +RND.SeedConsumer = csrng; + +// Use as default CSRNG +RND.FillBytes = csrng.GetBytes; +RND.FillBytesAsync = csrng.GetBytesAsync; +``` + +**NOTE**: When setting the `RND.FillBytes(Async)` callbacks, they may not be +used, if `/dev/urandom` was preferred. To disable `/dev/urandom`, set +`RND.UseDevUrandom` and `RND.RequireDevUrandom` to `false` also. + +**NOTE**: Currently only stream ciphers are supported, because the cipher RNG +implementation doesn't buffer pre-generated random data. diff --git a/src/wan24-Crypto-BC/RandomDataProvider.cs b/src/wan24-Crypto-BC/RandomDataProvider.cs new file mode 100644 index 0000000..96991da --- /dev/null +++ b/src/wan24-Crypto-BC/RandomDataProvider.cs @@ -0,0 +1,395 @@ +using Org.BouncyCastle.Crypto.Prng; +using wan24.Core; + +namespace wan24.Crypto.BC +{ + /// + /// Random data provider + /// + public class RandomDataProvider : RandomDataGenerator, IBouncyCastleRng // Note for myself: Do not try to move this to wan24-Crypto - it won't work... + { + /// + /// Random number generator + /// + protected readonly ISeedableRng RNG; + /// + /// RNG synchronization + /// + protected readonly SemaphoreSync RngSync = new(); + /// + /// Seed provider + /// + protected readonly RandomDataProvider? SeedProvider; + /// + /// Raised when seeded + /// + protected readonly AsyncEvent _OnSeedAsync; + /// + /// Did initialize? + /// + protected bool DidInit = false; + + /// + /// Constructor + /// + /// Buffer capacity in bytes + /// Random data provider to attach to (will be used for seeding, if available - otherwise fallback to ) + /// Initial seed length in bytes + /// Worker buffer size in bytes + /// RNG to use + public RandomDataProvider( + in int capacity, + in RandomDataProvider? rdp = null, + in int? seed = null, + in int? workerBufferSize = null, + in ISeedableRng? rng = null + ) + : this(rdp, capacity, workerBufferSize, rng) + { + if (seed.HasValue && seed.Value < 1) throw new ArgumentOutOfRangeException(nameof(seed)); + InitialSeed(seed ?? Settings.BufferSize); + } + + /// + /// Constructor + /// + /// Buffer capacity in bytes + /// Random data provider to attach to (will be used for seeding, if available - otherwise fallback to ) + /// Worker buffer size in bytes + /// RNG to use (will be disposed, if possible) + protected RandomDataProvider(in RandomDataProvider? rdp, in int capacity, in int? workerBufferSize, in ISeedableRng? rng = null) : base(capacity) + { + if (workerBufferSize.HasValue && workerBufferSize.Value < 1) throw new ArgumentOutOfRangeException(nameof(workerBufferSize)); + RNG = rng ?? new ChaCha20Rng(new BouncyCastleRngWrapper(new VmpcRandomGenerator()), byte.MaxValue); + _OnSeedAsync = new(this); + WorkerBufferSize = workerBufferSize ?? Settings.BufferSize; + UseFallback = false; + UseDevUrandom = false; + SeedProvider = rdp; + if (rdp is not null) + { + rdp.OnSeedAsync += HandleSeedAsync; + rdp.OnDisposing += HandleSeedProviderDisposing; + } + } + + /// + /// Raised when seeded + /// + public AsyncEvent OnSeedAsync + { + get => _OnSeedAsync; + set { } + } + + /// + /// Worker buffer size in bytes + /// + public int WorkerBufferSize { get; } + + /// + /// Create a fork instance, which attaches to this instances provided seeds + /// + /// Buffer capacity in bytes + /// Initial seed length in bytes + /// Worker buffer size in bytes + /// RNG to use (the RNG of this instance won't be used in the fork!) + /// Fork (service is not started yet; don't forget to dispose!) + public virtual RandomDataProvider CreateFork(in int? capacity = null, in int? seed = null, in int? workerBufferSize = null, in ISeedableRng? rng = null) + { + EnsureUndisposed(); + return new(capacity ?? RandomData.BufferSize, this, seed, workerBufferSize ?? WorkerBufferSize, rng); + } + + /// + /// Create a fork instance, which attaches to this instances provided seeds + /// + /// Buffer capacity in bytes + /// Initial seed length in bytes + /// Worker buffer size in bytes + /// RNG to use (the RNG of this instance won't be used in the fork!) + /// Cancellation token + /// Fork (service is not started yet; don't forget to dispose!) + public virtual Task CreateForkAsync( + int? capacity = null, + int? seed = null, + int? workerBufferSize = null, + ISeedableRng? rng = null, + CancellationToken cancellationToken = default) + { + EnsureUndisposed(); + return CreateAsync(capacity ?? RandomData.BufferSize, this, seed, workerBufferSize ?? WorkerBufferSize, rng, cancellationToken); + } + + /// + public override void AddSeed(ReadOnlySpan seed) + { + EnsureUndisposed(); + if (seed.Length == 0) return; + RNG.AddSeed(seed); + using RentedArrayStructSimple buffer = new(seed.Length); + seed.CopyTo(buffer.Span); + RaiseOnSeed(buffer.Memory); + } + + /// + public override async Task AddSeedAsync(ReadOnlyMemory seed, CancellationToken cancellationToken = default) + { + EnsureUndisposed(); + if (seed.Length == 0) return; + await RNG.AddSeedAsync(seed, cancellationToken).DynamicContext(); + await RaiseOnSeedAsync(seed,cancellationToken).DynamicContext(); + } + + /// + void IRandomGenerator.AddSeedMaterial(byte[] seed) => AddSeed(seed); + + /// + void IRandomGenerator.AddSeedMaterial(ReadOnlySpan seed) => AddSeed(seed); + + /// + void IRandomGenerator.AddSeedMaterial(long seed) + { + using RentedArrayRefStruct buffer = new(sizeof(long)); + seed.GetBytes(buffer.Span); + AddSeed(buffer.Span); + } + + /// + void IRandomGenerator.NextBytes(byte[] bytes) => Fill(bytes); + + /// + void IRandomGenerator.NextBytes(byte[] bytes, int start, int len) => Fill(bytes.AsSpan(start, len)); + + /// + void IRandomGenerator.NextBytes(Span bytes) => Fill(bytes); + + /// + /// Handle seed from the parent + /// + /// Random number provider + /// Arguments + /// Cancellation token + protected virtual async Task HandleSeedAsync(RandomDataProvider rnp, SeedEventArgs e, CancellationToken cancellationToken) + { + if (!DidInit) return; + try + { + await AddSeedAsync(e.Seed, cancellationToken).DynamicContext(); + } + catch (Exception ex) + { + ErrorHandling.Handle(new( + $"Failed to seed a {GetType()} instance (\"{Name}\") from seed provider {rnp.GetType()} (\"{rnp.Name}\")", + ex, + Constants.CRYPTO_ERROR_SOURCE + )); + } + } + + /// + /// Handle a disposing seed provider + /// + /// Sender + /// Arguments + protected virtual void HandleSeedProviderDisposing(IDisposableObject sender, EventArgs e) => Dispose(); + + /// + /// Perform initial seeding + /// + /// Initial seed length in bytes + protected virtual void InitialSeed(in int len) + { + using RentedArrayRefStruct buffer = new(len); + if (SeedProvider is null) + { + RND.FillBytes(buffer.Span); + } + else + { + SeedProvider.FillBytes(buffer.Span); + } + RNG.AddSeed(buffer.Span); + DidInit = true; + } + + /// + /// Perform initial seeding + /// + /// Initial seed length in bytes + /// Cancellation token + protected virtual async Task InitialSeedAsync(int len, CancellationToken cancellationToken = default) + { + using RentedArrayStructSimple buffer = new(len); + if (SeedProvider is null) + { + await RND.FillBytesAsync(buffer.Memory).DynamicContext(); + } + else + { + await SeedProvider.FillBytesAsync(buffer.Memory, cancellationToken).DynamicContext(); + } + await RNG.AddSeedAsync(buffer.Memory, cancellationToken).DynamicContext(); + DidInit = true; + } + + /// + protected override async Task WorkerAsync() + { + bool isDefaultRng = RND.Generator == this; + using RentedArrayStructSimple buffer1 = new(WorkerBufferSize, clean: false) + { + Clear = true + }; + if (isDefaultRng)// Avoids an endless recursion when using this instance as RND.Generator + { + for (; !CancelToken.IsCancellationRequested;) + { + await RngSync.ExecuteAsync(async () => + { + await RNG.FillBytesAsync(buffer1.Memory, CancelToken).DynamicContext(); + return Task.CompletedTask; + }, CancelToken).DynamicContext(); + CancelToken.ThrowIfCancellationRequested(); + await RandomData.WriteAsync(buffer1.Memory, CancelToken).DynamicContext(); + } + } + else + { + using RentedArrayStructSimple buffer2 = new(WorkerBufferSize, clean: false) + { + Clear = true + }; + for (; !CancelToken.IsCancellationRequested;) + { + await RngSync.ExecuteAsync(async () => + { + await RNG.FillBytesAsync(buffer1.Memory, CancelToken).DynamicContext(); + return Task.CompletedTask; + }, CancelToken).DynamicContext(); + CancelToken.ThrowIfCancellationRequested(); + await RND.DefaultRngAsync(buffer2.Memory).DynamicContext(); + CancelToken.ThrowIfCancellationRequested(); + buffer1.Span.Xor(buffer2.Span); + await RandomData.WriteAsync(buffer1.Memory, CancelToken).DynamicContext(); + } + } + } + + /// + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (SeedProvider is not null) + { + SeedProvider!.OnDisposing -= HandleSeedProviderDisposing; + SeedProvider.OnSeedAsync -= HandleSeedAsync; + } + RngSync.Dispose(); + RNG.TryDispose(); + } + + /// + protected override async Task DisposeCore() + { + await base.DisposeCore().DynamicContext(); + if (SeedProvider is not null) + { + SeedProvider!.OnDisposing -= HandleSeedProviderDisposing; + SeedProvider.OnSeedAsync -= HandleSeedAsync; + } + await RngSync.DisposeAsync().DynamicContext(); + await RNG.TryDisposeAsync().DynamicContext(); + } + + /// + /// Delegate for a seed handler + /// + /// Random number provider + /// Arguments + public delegate void Seed_Delegate(RandomDataProvider rnp, SeedEventArgs e); + /// + /// Raised when seeded + /// + public event Seed_Delegate? OnSeed; + /// + /// Raise the event + /// + /// Seed + protected virtual void RaiseOnSeed(in ReadOnlyMemory seed) + { + SeedEventArgs e = new(seed); + Task task = OnSeedAsync.Abstract.RaiseEventAsync(this, e, cancellationToken: CancelToken); + OnSeed?.Invoke(this, e); + task.Wait(); + } + /// + /// Raise the event + /// + /// Seed + /// Cancellation token + protected virtual async Task RaiseOnSeedAsync(ReadOnlyMemory seed, CancellationToken cancellationToken) + { + SeedEventArgs e = new(seed); + Task task = OnSeedAsync.Abstract.RaiseEventAsync(this, e, cancellationToken: CancelToken); + OnSeed?.Invoke(this, e); + await task.DynamicContext(); + } + + /// + /// Create an instance + /// + /// Buffer capacity in bytes + /// Random data provider to attach to (will be used for seeding, if available - otherwise fallback to ) + /// Initial seed length in bytes + /// Worker buffer size in bytes + /// RNG to use + /// Cancellation token + /// Instance (service is not started yet; don't forget to dispose!) + public static async Task CreateAsync( + int capacity, + RandomDataProvider? rdp = null, + int? seed = null, + int? workerBufferSize = null, + ISeedableRng? rng = null, + CancellationToken cancellationToken = default + ) + { + if (seed.HasValue && seed.Value < 1) throw new ArgumentOutOfRangeException(nameof(seed)); + if (workerBufferSize.HasValue && workerBufferSize.Value < 1) throw new ArgumentOutOfRangeException(nameof(workerBufferSize)); + RandomDataProvider res = new(rdp, capacity, workerBufferSize, rng); + try + { + await res.InitialSeedAsync(seed ?? Settings.BufferSize, cancellationToken).DynamicContext(); + return res; + } + catch + { + await res.DisposeAsync().DynamicContext(); + throw; + } + } + + /// + /// event arguments + /// + public class SeedEventArgs : EventArgs + { + /// + /// Constructor + /// + public SeedEventArgs() : base() { } + + /// + /// Constructor + /// + /// Seed + public SeedEventArgs(in ReadOnlyMemory seed) : this() => Seed = seed; + + /// + /// Seed + /// + public ReadOnlyMemory Seed { get; } = null!; + } + } +} diff --git a/src/wan24-Crypto-BC/StreamCipherRng.cs b/src/wan24-Crypto-BC/StreamCipherRng.cs new file mode 100644 index 0000000..31e40a8 --- /dev/null +++ b/src/wan24-Crypto-BC/StreamCipherRng.cs @@ -0,0 +1,196 @@ +using Org.BouncyCastle.Crypto.Prng; +using wan24.Core; + +namespace wan24.Crypto.BC +{ + /// + /// Stream cipher CSRNG + /// + public class StreamCipherRng : DisposableSeedableRngBase, IBouncyCastleRng + { + /// + /// RNG synchronization + /// + protected readonly SemaphoreSync RngSync = new(); + /// + /// Buffer + /// + protected readonly BlockingBufferStream Buffer = null!; + /// + /// Cipher stream + /// + protected readonly EncryptionStreams Encryption = null!; + /// + /// Internal RNG to use + /// + protected readonly ISeedableRng RNG = null!; + + /// + /// Constructor + /// + /// Encryption algorithm to use + /// Internal RNG to use (will be disposed, if possible!) + /// Buffer size in bytes (min. the IV byte length of the underlaying cipher) + /// Seed the given RNG with N byte from (skipped, if or <1) + public StreamCipherRng( + in EncryptionAlgorithmBase algorithm, + ISeedableRng? rng = null, + in int? bufferSize = null, + in int? seedLength = 256 + ) + : base(asyncDisposing: false) + { + Algorithm = algorithm; + try + { + if (algorithm.BlockSize != 1) throw new ArgumentException("Stream cipher required", nameof(algorithm)); + if (bufferSize.HasValue && bufferSize.Value < Algorithm.IvSize) + throw new ArgumentOutOfRangeException(nameof(bufferSize), $"Min. buffer size for {algorithm.DisplayName} is {algorithm.IvSize} byte"); + rng ??= new BouncyCastleRngWrapper(new VmpcRandomGenerator()); + // Create a buffer for chunking random sequences + Buffer = new(bufferSize ?? Settings.BufferSize, clear: true); + // Seed the RNG + if (seedLength > 0) + using (RentedArray seed = new(len: seedLength.Value, clean: false) + { + Clear = true + }) + { + RND.FillBytes(seed.Span); + rng.AddSeed(seed.Span); + } + // Create a random key + using SecureByteArrayRefStruct key = new(algorithm.KeySize); + rng.FillBytes(key.Span); + // Create encryption options + CryptoOptions options = new CryptoOptions() + { + Password = key.Array + } + .IncludeNothing() + .WithoutMac() + .WithoutKdf() + .WithoutCompression(); + options.FlagsIncluded = false; + // Create the cipher stream + Encryption = Algorithm.GetEncryptionStream(Stream.Null, Buffer, macStream: null, options); + // Remove the written IV bytes from the buffer + if (algorithm.IvSize > 0) + { + if (Buffer.Available != algorithm.IvSize) + throw CryptographicException.From( + $"{algorithm.DisplayName} stream initialization failed: {Buffer.Available} IV byte buffered ({algorithm.IvSize} byte expected)", + new InvalidProgramException() + ); + using RentedArray buffer = new(algorithm.IvSize, clean: false) + { + Clear = true + }; + int red = Buffer.TryRead(buffer.Span); + if (red != algorithm.IvSize) + throw CryptographicException.From( + $"{algorithm.DisplayName} stream initialization failed: {Buffer.Available} IV byte buffered, but only {red} byte red", + new InvalidProgramException() + ); + } + // Store the internal RNG to use + RNG = rng; + } + catch + { + Dispose(); + rng.TryDispose(); + throw; + } + } + + /// + /// Encryption algorithm to use + /// + public EncryptionAlgorithmBase Algorithm { get; } + + /// + public override void AddSeed(ReadOnlySpan seed) => AddSeedMaterial(seed); + + /// + public override async Task AddSeedAsync(ReadOnlyMemory seed, CancellationToken cancellationToken = default) + { + EnsureUndisposed(); + using SemaphoreSyncContext ssc = await RngSync.SyncContextAsync(cancellationToken).DynamicContext(); + await RNG.AddSeedAsync(seed, cancellationToken).DynamicContext(); + } + + /// + public virtual void AddSeedMaterial(byte[] seed) + { + EnsureUndisposed(); + using SemaphoreSyncContext ssc = RngSync; + RNG.AddSeed(seed); + } + + /// + public virtual void AddSeedMaterial(ReadOnlySpan seed) + { + EnsureUndisposed(); + using SemaphoreSyncContext ssc = RngSync; + RNG.AddSeed(seed); + } + + /// + public virtual void AddSeedMaterial(long seed) + { + EnsureUndisposed(); + using SemaphoreSyncContext ssc = RngSync; + using RentedArrayRefStruct buffer = new(sizeof(long)); + seed.GetBytes(buffer.Span); + RNG.AddSeed(buffer.Span); + } + + /// + public override Span FillBytes(in Span buffer) + { + NextBytes(buffer); + return buffer; + } + + /// + public override Task> FillBytesAsync(Memory buffer, CancellationToken cancellationToken = default) + { + NextBytes(buffer.Span); + return Task.FromResult(buffer); + } + + /// + public void NextBytes(byte[] bytes) => NextBytes(bytes.AsSpan()); + + /// + public void NextBytes(byte[] bytes, int start, int len) => NextBytes(bytes.AsSpan(start, len)); + + /// + public virtual void NextBytes(Span bytes) + { + EnsureUndisposed(); + if (bytes.Length == 0) return; + using SemaphoreSyncContext ssc = RngSync; + for (int write; bytes.Length != 0; bytes = bytes[write..]) + { + write = Math.Min(bytes.Length, Buffer.BufferSize); + RNG.FillBytes(bytes[..write]); + Encryption.CryptoStream.Write(bytes[..write]); + Encryption.CryptoStream.Flush(); + if (Buffer.Read(bytes[..write]) != write) + throw CryptographicException.From("Failed to read all random data from the buffer", new InvalidProgramException()); + } + } + + /// + protected override void Dispose(bool disposing) + { + using SemaphoreSync rngSync = RngSync; + using SemaphoreSyncContext ssc = rngSync; + using Stream buffer = Buffer; + using EncryptionStreams chaCha = Encryption; + RNG?.TryDispose(); + } + } +} diff --git a/src/wan24-Crypto-BC/XSalsa20Rng.cs b/src/wan24-Crypto-BC/XSalsa20Rng.cs new file mode 100644 index 0000000..b993916 --- /dev/null +++ b/src/wan24-Crypto-BC/XSalsa20Rng.cs @@ -0,0 +1,20 @@ +using Org.BouncyCastle.Crypto.Prng; + +namespace wan24.Crypto.BC +{ + /// + /// XSalsa20 CSRNG + /// + public sealed class XSalsa20Rng : StreamCipherRng + { + /// + /// Constructor + /// + /// Internal RNG to use (will be disposed, if possible!) + /// Buffer size in bytes (min. ) + /// Seed the given RNG with N byte from (skipped, if or <1) + public XSalsa20Rng(in ISeedableRng? rng = null, in int? bufferSize = null, in int? seedLength = 256) + : base(EncryptionXSalsa20Algorithm.Instance, rng, bufferSize, seedLength) + { } + } +} diff --git a/src/wan24-Crypto-BC/wan24-Crypto-BC.csproj b/src/wan24-Crypto-BC/wan24-Crypto-BC.csproj index 0b5a9ab..f7e7aeb 100644 --- a/src/wan24-Crypto-BC/wan24-Crypto-BC.csproj +++ b/src/wan24-Crypto-BC/wan24-Crypto-BC.csproj @@ -33,8 +33,8 @@ - - + +