Marai Cryptography

NuGet Stable License

Version: 1.0.3
Target Framework: net10.0+
NuGet Package ID: Marai.Cryptography


Introduction

Marai.Cryptography is a production-ready cryptography library for .NET providing AES-256 encryption across multiple modes, HMAC-SHA256 request signing, and secure PBKDF2 password hashing with a clean, consistent API.

Major capabilities:

  • AES-256 encryption and decryption in GCM, CBC, CTR, CFB, and GCMFormatted modes
  • SHA-256 hashing of arbitrary text
  • Key and IV generation for AES-256
  • AES keys, RSA public/private key pairs, AES and legacy TDES DUKPT BDKs, and random password generation
  • HMAC-SHA256 request signature creation and validation with replay-attack protection
  • Secure PBKDF2/SHA-256 password hashing with randomized salt and iteration count
  • Constant-time comparison for signature and password verification
  • Microsoft Dependency Injection integration

Table of Contents


Installation

1 dotnet add package Marai.Cryptography
1 Install-Package Marai.Cryptography

Getting Started

Register with Dependency Injection

1 2 3 4 5 6 7 8 9 using Marai.Cryptography.DependencyInjection; // With a pre-configured key and IV (required for EncryptGcm / DecryptGcm) builder.Services.AddMaraiCryptography( key: “your-base64-encoded-256-bit-key==”, vector: “your-base64-encoded-iv==”); // Without a key (only explicit-key overloads will work) builder.Services.AddMaraiCryptography();

AddMaraiCryptography registers:

  • ISecurityAESProvider (singleton) — encryption and hashing
  • ISecurityHMACSignatureProvider (singleton) — HMAC request signing
  • ISecurityPasswordProvider (singleton) — password hashing
  • ISecurityKeyGeneratorProvider (singleton) — key and password generation; no configured key or IV required

Generate a Key and IV

1 2 3 var (key, iv) = aesProvider.GenerateAes256KeyAndIv(); Console.WriteLine($”Key: {key}”); Console.WriteLine($”IV: {iv}”);

AES Provider

Encryption Modes

Value Name Description Recommended
GCMFormatted AES-256-GCM with header GCM with magic-byte prefix for format detection. Layout: [GCM(3)][nonce(12)][tag(16)][ciphertext] Yes — default
GCM AES-256-GCM raw Authenticated GCM without format header. Layout: [nonce(12)][tag(16)][ciphertext] Yes
CBC AES-256-CBC Block cipher with PKCS7 padding. Layout: [iv(16)][ciphertext] Acceptable
CTR AES-256-CTR Stream cipher mode. Layout: [counter(16)][ciphertext] Acceptable
CFB AES-256-CFB Cipher Feedback mode (CFB-128). Layout: [iv(16)][ciphertext] Acceptable
ECB AES-256-ECB No IV. Identical plaintext blocks produce identical ciphertext. Deprecated. No
ECB is marked [Obsolete] in the library. Do not use it for any data where block-level patterns could be revealing.

Default Encrypt and Decrypt

EncryptGcm and DecryptGcm use the key and IV passed to AddMaraiCryptography.

1 2 3 4 5 6 7 8 9 10 11 12 public class DataService(ISecurityAESProvider aes) { public string StoreSecret(string plainText) { return aes.EncryptGcm(plainText); } public string ReadSecret(string encrypted) { return aes.DecryptGcm(encrypted); } }

Explicit Mode Encrypt and Decrypt

1 2 3 4 5 6 7 8 // Encrypt with GCMFormatted (default) string encrypted = aes.Encrypt(plainText, myKeyBase64, EncryptionFormats.GCMFormatted, myIvBase64); // Decrypt string decrypted = aes.Decrypt(encrypted, myKeyBase64, EncryptionFormats.GCMFormatted, myIvBase64); // Encrypt with CBC string cbcEncrypted = aes.Encrypt(plainText, myKeyBase64, EncryptionFormats.CBC, myIvBase64);
Keys and IVs are passed as strings. The library derives a fixed-length byte array via SHA-256. Use GenerateAes256KeyAndIv() to produce a proper random key.

SHA-256 Hash

1 2 string hash = aes.SHA256Hash(“hello world”); // Returns lowercase hex string, e.g. “b94d27b9…”

Key and IV Generation

1 2 3 var (keyBase64, ivBase64) = aes.GenerateAes256KeyAndIv(); // keyBase64 — 256-bit key (32 bytes), Base64-encoded // ivBase64 — 128-bit IV (16 bytes), Base64-encoded

HMAC Signature Provider

Creating a Signature

1 2 3 var secretKey = new HmacSecretKey(Base64: mySecretBase64); string signature = hmac.CreateSignature(requestPayloadJson, secretKey); // Returns a Base64-encoded HMAC-SHA256 signature string

The payload must be valid JSON. The library wraps it with the current UTC Unix timestamp before signing:

1 2 3 4 { “TimeStamp”: 1700000000, “RequestPayLoad”: { } }

Validating a Signature

1 2 3 4 5 6 7 8 var request = new HmacSignedRequest( Payload: requestPayloadJson, Signature: receivedSignature, TimeStamp: new UnixTimeSeconds(receivedTimestamp), SecretKey: new HmacSecretKey(mySecretBase64)); bool valid = hmac.ValidateSignature(request); // Returns true on success; throws on failure

ValidateSignature steps:

  1. Recomputes the expected HMAC using the payload and timestamp.
  2. Compares using constant-time comparison (CryptographicOperations.FixedTimeEquals).
  3. Validates the timestamp is within the allowed clock skew and expiration window.

Default timing windows: AllowedClockSkewInMinutes = 5, AllowedExpirationInMinutes = 5.


Password Provider

Hashing a Password

1 2 PasswordHash hash = passwordProvider.SecurePassword(“my-password”); // Store hash.HashBase64, hash.SaltBase64, hash.Iterations in your database

Validating a Password

1 2 3 4 5 6 7 var stored = new PasswordHash( HashBase64: storedHashFromDb, SaltBase64: storedSaltFromDb, Iterations: storedIterationsFromDb); bool isValid = passwordProvider.IsValidPassword(“my-password”, stored);

Key Generator Provider

SecurityKeyGeneratorProvider in Marai.Cryptography.Implementations implements ISecurityKeyGeneratorProvider in Marai.Cryptography.Abstractions. It generates fresh software key material and passwords without retaining them as instance state.

using Marai.Cryptography.Implementations; var generator = new SecurityKeyGeneratorProvider(); string aesKey = generator.GenerateAesKey(); var (publicKeyPem, privateKeyPem) = generator.GenerateRsaKeyPair(); string aesBdk = generator.GenerateAesBdk(); string password = generator.GeneratePassword(); string pciPassword = generator.GeneratePciPassword(); // Explicit sizes and lengths: string aes128 = generator.GenerateAesKey(keySizeInBits: 128); var rsa4096 = generator.GenerateRsaKeyPair(keySizeInBits: 4096); string longerPassword = generator.GeneratePassword(length: 32);
MethodDefault / accepted inputOutput
GenerateAesKey(keySizeInBits)256 bits; 128, 192, or 256Base64 encoding of 16, 24, or 32 raw key bytes
GenerateRsaKeyPair(keySizeInBits)3072 bits; 2048, 3072, or 4096Matching (PublicKeyPem, PrivateKeyPem) tuple
GenerateAesBdk(keySizeInBits)256 bits; 128, 192, or 25632, 48, or 64 uppercase hex characters
GenerateBdk()Legacy two-key TDES; obsolete warning16 bytes as 32 uppercase hex characters, with odd parity and weak-key rejection
GeneratePassword(length)24 characters; 15–1024Random password without character-class requirements
GeneratePciPassword(length)24 characters; 15–1024Random password guaranteed to contain both letters and digits

Unsupported sizes and lengths throw ArgumentOutOfRangeException. Cryptographic provider failures propagate; no weaker random-source fallback is used.

Dependency Injection

using Marai.Cryptography.Abstractions; using Marai.Cryptography.DependencyInjection; using Microsoft.Extensions.DependencyInjection; var services = new ServiceCollection(); services.AddMaraiCryptography(); using var serviceProvider = services.BuildServiceProvider(); var generator = serviceProvider.GetRequiredService<ISecurityKeyGeneratorProvider>(); string key = generator.GenerateAesKey();

Applications already calling AddMaraiCryptography() can inject ISecurityKeyGeneratorProvider into consuming classes. This registration adds no new runtime package dependency.

How Generation Works

  • AES and AES BDK: RandomNumberGenerator.GetBytes() supplies random key bytes. The output encodings differ; no password derivation is involved.
  • RSA: RSA.Create(keySizeInBits) generates one matching pair through the platform provider. The public output is SubjectPublicKeyInfo PEM (PUBLIC KEY); the private output is unencrypted PKCS#8 PEM (PRIVATE KEY). Import with RSA.ImportFromPem(). The RSA instance is disposed after export.
  • TDES: random bytes are adjusted to odd DES parity. Generation retries for degenerate TDES keys or weak/semi-weak DES components. The 16-byte representation includes parity bits and does not represent 128 bits of security.
  • Passwords: RandomNumberGenerator.GetInt32() selects characters without modulo bias from uppercase/lowercase ASCII letters, digits, and !@#$%^&*()-_=+[]{}:,.?. The PCI variant retries complete candidates until letters and digits are both present. Neither method guarantees all four character classes.
  • Memory: temporary symmetric key/component buffers are zeroed and password character buffers are cleared. Returned strings cannot be explicitly wiped and must not be logged.

Legacy TDES Support

// Supported legacy call; intentionally emits compiler warning CS0618. string legacyTdesBdk = generator.GenerateBdk();

GenerateBdk() is enabled. Both interface and implementation use [Obsolete(..., false)]; the method no longer intentionally throws NotSupportedException. Builds treating warnings as errors may still reject calls. Use AES BDKs for new AES DUKPT integrations, but migrate the receiving system first: TDES and AES key formats are not interchangeable.

Key Formats and Compatibility

Decode GenerateAesKey() with Convert.FromBase64String() when supplying raw bytes to .NET AES APIs. The existing SecurityAESProvider hashes its string key input, so passing Base64 to that provider produces a different effective key from using the decoded bytes directly. This behavior and GenerateAes256KeyAndIv() are unchanged. The new generator does not manage per-message IVs/nonces, rotate stored keys, or derive DUKPT initial/transaction keys or HSM key blocks.

Standards Basis and Deployment Limits

The implementation review referenced NIST SP 800-133 Rev. 2 for key generation, SP 800-131A Rev. 2 for algorithm/key-length choices, SP 800-63B-4 for password guidance, and PCI DSS v4.0.1 requirements 3.7.1 and 8.3.6. NIST’s verifier composition guidance and PCI’s defined password requirements differ, hence the separate password generators. Legacy TDES support does not reverse NIST’s withdrawal of TDEA approval for new protection.

This library does not establish FIPS validation or PCI compliance. Verify the deployed cryptographic module and its approved configuration where required. Production PCI PIN/DUKPT BDKs need an appropriate approved HSM/secure cryptographic device workflow; both BDK methods here export plaintext into application memory. Secure storage, distribution, rotation, access control, and applicable dual control/split knowledge remain deployment responsibilities. Password generation does not implement hashing, blocklists, or rate limiting. Existing encryption and password hashing were not certified by this generator review.


Models Reference

EncryptionFormats

1 2 3 4 5 6 7 8 9 public enum EncryptionFormats { GCM = 1, GCMFormatted = 2, CBC = 3, CTR = 4, [Obsolete] ECB = 5, CFB = 6 }

HmacSecretKey

Member Type Description
Base64 string Base64-encoded HMAC secret key bytes
ToBytes() byte[] Decodes the key to a raw byte array; throws if blank

HmacSignedRequest

Member Type Description
Payload string The original JSON request payload
Signature string Base64-encoded HMAC-SHA256 signature to validate
TimeStamp UnixTimeSeconds Unix timestamp when the signature was created
SecretKey HmacSecretKey The shared secret key
allowedClockSkewInMinutes int Clock skew tolerance (default: 0, uses config value of 5)
allowedExpirationInMinutes int Max request age in minutes (default: 0, uses config value of 5)

PasswordHash

Member Type Description
HashBase64 string Base64-encoded PBKDF2 hash output
SaltBase64 string Base64-encoded random salt
Iterations int PBKDF2 iteration count used when hashing

UnixTimeSeconds

Member Type Description
Value long Unix timestamp in seconds
Now() UnixTimeSeconds (static) Returns current UTC time as UnixTimeSeconds
ToDateTimeOffset() DateTimeOffset Converts to UTC DateTimeOffset

Exceptions Reference

Exception Thrown When
InvalidSignatureException HMAC signature does not match
ExpiredSignatureException Request timestamp is outside the allowed window
InvalidParameterException A required parameter has an invalid value
ParameterNotFoundException A required parameter is missing
UnknownErrorException An unexpected error occurs
1 2 3 4 5 6 7 8 9 10 11 12 try { bool valid = hmac.ValidateSignature(request); } catch (InvalidSignatureException) { // Signature mismatch — reject the request } catch (ExpiredSignatureException) { // Timestamp too old or too far in the future — reject }

Security Notes

  • AES-256-GCM / GCMFormatted — Use for all new encryption. GCM provides authenticated encryption, detecting tampering automatically.
  • AES-256-CBC / CFB / CTR — Acceptable for interoperability. No built-in authentication; combine with a separate MAC if tamper detection is needed.
  • AES-256-ECB — Do not use. Marked [Obsolete] in the library.

Key and IV Management

  • Never hardcode keys in source code. Store them in environment variables, Azure Key Vault, AWS Secrets Manager, or similar.
  • Use GenerateAes256KeyAndIv() to produce cryptographically secure random keys and IVs.

Signature Security

  • Uses CryptographicOperations.FixedTimeEquals to prevent timing side-channels.
  • The 5-minute expiration window blocks replay attacks. Tune allowedExpirationInMinutes as needed.
  • The secret key should be at least 256 bits (32 bytes) of random data.

Password Hashing

  • Uses PBKDF2 with SHA-256, a random 16-byte salt, and a random iteration count between 5,000 and 10,000.
  • Password comparison uses constant-time comparison to prevent timing attacks.

Full API Reference

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 // AES Provider interface ISecurityAESProvider { string EncryptGcm(string plainText); // Encrypt using injected key/IV string DecryptGcm(string plainText); // Decrypt using injected key string Encrypt(string plainText, string key, EncryptionFormats format = GCMFormatted, string? iv = null); string Decrypt(string encryptedText, string key, EncryptionFormats format = GCMFormatted, string? iv = null); string SHA256Hash(string text); // SHA-256 hex hash (string KeyBase64, string IvBase64) GenerateAes256KeyAndIv(); // Generate random key+IV } // HMAC Signature Provider interface ISecurityHMACSignatureProvider { string CreateSignature(string requestPayload, HmacSecretKey secretKey); // Create HMAC-SHA256 signature bool ValidateSignature(HmacSignedRequest request); // Validate; throws on failure } // Password Provider interface ISecurityPasswordProvider { PasswordHash SecurePassword(string password); // Hash a password bool IsValidPassword(string password, PasswordHash stored); // Verify a password } // DI Registration static IServiceCollection AddMaraiCryptography(this IServiceCollection services, string key = “”, string vector = “”); // Models readonly record struct HmacSecretKey(string Base64) { byte[] ToBytes(); } sealed record HmacSignedRequest(string Payload, string Signature, UnixTimeSeconds TimeStamp, HmacSecretKey SecretKey, int allowedClockSkewInMinutes = 0, int allowedExpirationInMinutes = 0); readonly record struct PasswordHash(string HashBase64, string SaltBase64, int Iterations); readonly record struct UnixTimeSeconds(long Value) { static UnixTimeSeconds Now(); DateTimeOffset ToDateTimeOffset(); } enum EncryptionFormats { GCM = 1, GCMFormatted = 2, CBC = 3, CTR = 4, [Obsolete] ECB = 5, CFB = 6 } // Exceptions (all extend Exception) sealed class InvalidSignatureException : Exception { } sealed class ExpiredSignatureException : Exception { } sealed class InvalidParameterException : Exception { } sealed class ParameterNotFoundException : Exception { } sealed class UnknownErrorException : Exception { } // Configuration constants static class SecurityRSASignatureConfiguration { const int AllowedClockSkewInMinutes = 5; const int AllowedExpirationInMinutes = 5; }

Key Generator API

namespace Marai.Cryptography.Abstractions; public interface ISecurityKeyGeneratorProvider { /// <summary>Generates a Base64 AES key of 128, 192, or 256 bits.</summary> string GenerateAesKey(int keySizeInBits = 256); /// <summary>Generates matching SubjectPublicKeyInfo public and unencrypted PKCS#8 private PEM keys.</summary> (string PublicKeyPem, string PrivateKeyPem) GenerateRsaKeyPair(int keySizeInBits = 3072); /// <summary>Generates a legacy 16-byte, odd-parity, two-key TDES DUKPT BDK as uppercase hex, rejecting weak keys.</summary> [Obsolete("TDES is no longer NIST-approved for protecting new data. For new systems, use GenerateAesBdk for AES DUKPT.", false)] string GenerateBdk(); /// <summary>Generates an AES DUKPT BDK of 128, 192, or 256 bits as uppercase hex.</summary> string GenerateAesBdk(int keySizeInBits = 256); /// <summary>Generates a uniformly random password without composition constraints; length 15–1024.</summary> string GeneratePassword(int length = 24); /// <summary>Generates a random password containing letters and digits for PCI DSS 8.3.6; length 15–1024.</summary> string GeneratePciPassword(int length = 24); }

License

This project is licensed under the Marai Proprietary Software License Agreement.

Free for personal and commercial use.

For the complete license agreement and terms, please visit: https://marai.dev/proprietary-software-license-agreement


Recent Changes — September 12, 2026

  • Added SecurityKeyGeneratorProvider, its interface, singleton DI registration, and six generation methods for AES, RSA, AES/TDES BDKs, and passwords.
  • Kept AES-256 and RSA-3072 defaults; documented supported sizes and output formats.
  • Restored legacy TDES generation after its temporary disabling. It now generates keys with an obsolete compiler warning.
  • Added GeneratePciPassword() and clarified password-generation policies and deployment limits.
  • Updated README and this guide with examples, implementation details, and compatibility notes.

Breaking Changes and Migration

The following changes affect callers of the earlier key-generator implementation.

ChangeImpactDeveloper action
Password minimum increased from 12 to 15GeneratePassword(12) through GeneratePassword(14) now throw ArgumentOutOfRangeException. Both generators accept 15–1024.Use at least 15 characters or keep the 24-character default. This does not change validation of stored password hashes.
Default composition guarantee removedGeneratePassword() no longer guarantees uppercase, lowercase, digits, and symbols in every result.Remove assumptions about character classes. Use GeneratePciPassword() for guaranteed letters and digits; it does not guarantee all four classes.
TDES marked obsolete on both API declarationsCalls emit CS0618; warnings-as-errors builds may fail.Plan migration to AES DUKPT. For an intentional legacy integration, handle the warning according to project policy. Do not substitute AES keys into a TDES system.
TDES generation restoredThe earlier intentional NotSupportedException is removed; callers now receive a generated BDK.Remove assumptions that the legacy entry point is disabled. Ordinary platform failures can still propagate.
Interface gained generation membersCustom implementations of the earlier ISecurityKeyGeneratorProvider contract must implement added members, including GeneratePciPassword().Update custom providers, fakes, and mocks when adopting the current interface.

Existing AES encryption, HMAC signing, password hashing, stored data formats, and GenerateAes256KeyAndIv() remain unchanged by the generator update. No automatic key rotation or data migration is performed.

Verification

dotnet run --project Tests/KeyGenerationChecks/KeyGenerationChecks.csproj

The regression console checks DI resolution, defaults and accepted key sizes, AES/RSA round trips, RSA signature verification, password bounds and PCI output rules, TDES parity/weak-key rejection and round trips, and warning-only obsolete attributes. Repeated-output checks detect accidental constant output; they do not certify randomness or compliance.