cryptobase.psm1
|
#!/usr/bin/env pwsh using namespace System using namespace System.IO using namespace System.Web using namespace System.Text using namespace System.Net.Http using namespace System.Security using namespace System.Reflection using namespace System.Globalization using namespace System.Reflection.Emit using namespace System.Runtime.Serialization using namespace System.Security.Cryptography using namespace System.Runtime.InteropServices using namespace System.Collections.ObjectModel # Load all sub-modules : # (Get-ChildItem ./Private).Name.ForEach({ "using module Private/" + $_ }) using module Private/Enums.psm1 using module Private/Exceptions.psm1 using module Private/Utilities.psm1 using module Private/AesCCM.psm1 using module Private/AesCfb.psm1 using module Private/AesCmac.psm1 using module Private/AesCng.psm1 using module Private/AesCtr.psm1 using module Private/AesGCM.psm1 using module Private/AesOcb.psm1 using module Private/AesSIV.psm1 using module Private/Armor.psm1 using module Private/BCrypt.psm1 using module Private/Blake2b.psm1 using module Private/ChaCha20.psm1 using module Private/Crc24.psm1 using module Private/Credentials.psm1 using module Private/Curve25519.psm1 using module Private/Ecdsa.psm1 using module Private/EdwardsCurve.psm1 using module Private/EllipticCurve.psm1 using module Private/Models.psm1 using module Private/FileMonitor.psm1 using module Private/Hc128.psm1 using module Private/Hc256.psm1 using module Private/Hkdf.psm1 using module Private/KeypairGen.psm1 using module Private/KMACAuth.psm1 using module Private/MD5.psm1 using module Private/opaque.psm1 using module Private/OpenPgp.psm1 using module Private/OTPKIT.psm1 using module Private/PasswordHashing.psm1 using module Private/Pbkdf2.psm1 using module Private/PostQuantum.psm1 using module Private/Rabbit.psm1 using module Private/RSA.psm1 using module Private/S2K.psm1 using module Private/Secp256k1.psm1 using module Private/Sha.psm1 using module Private/TripleDES.psm1 using module Private/Vault.psm1 using module Private/X509.psm1 using module Private/XChaCha20Poly1305.psm1 using module Private/XOR.psm1 using module Private/XSalsa20.psm1 #Requires -PSEdition Core #Requires -Modules PsModuleBase, cliHelper.xconvert # Main class class CryptoBase : CryptobaseUtils { static [Type[]] $ReturnTypes = ([CryptoBase]::Methods.ReturnType | Sort-Object -Unique Name) static [MethodInfo[]] $Methods = ([Cryptobase].GetMethods().Where({ $_.IsStatic -and !$_.IsHideBySig })) CryptoBase() {} static [string] GetHelp() { return @" NAME CryptoBase (Invoke-CryptoBase) - Cryptographic utility tool SYNOPSIS Invoke-CryptoBase <Method> [-InputObject <Object>] <Object> | cryptobase <Method> DESCRIPTION Provides high-level convenience methods for common cryptographic tasks such as encryption, decryption, signing, and file obfuscation. When using pipeline input, you can use the single-parameter method overloads which will prompt you for any required passwords interactively. METHODS GetHelp Shows this help information. ProtectData Encrypts data using Argon2id and AES-256-GCM. Pipeline: "secret" | cryptobase ProtectData UnprotectData Decrypts data previously encrypted with ProtectData. Pipeline: `$encryptedBytes | cryptobase UnprotectData ProtectDataCascade Paranoid cascade mode encryption (AES-256-GCM + XChaCha20-Poly1305). Pipeline: "secret" | cryptobase ProtectDataCascade UnprotectDataCascade Decrypts cascade mode payloads. Pipeline: `$encryptedBytes | cryptobase UnprotectDataCascade SignMessage Signs a message using Secp256k1 and returns the signature and keys. Pipeline: "message" | cryptobase SignMessage ObfuscateFile Obfuscates a file with CRC24 integrity. Prompts for password. Pipeline: "source.txt" | cryptobase ObfuscateFile DeobfuscateFile Deobfuscates a file. Prompts for password. Pipeline: "source.txt.enc" | cryptobase DeobfuscateFile EXAMPLES "This is my secret message" | cryptobase SignMessage "Sensitive Data" | cryptobase ProtectData > secret.bin Get-Content secret.bin -AsByteStream | cryptobase UnprotectData "@ } static [byte[]] ProtectData([string]$plaintext) { return [CryptoBase]::ProtectData([Encoding]::UTF8.GetBytes($plaintext), [CryptoBase]::ReadSecureString("Password")) } static [byte[]] ProtectData([byte[]]$plainbytes) { return [CryptoBase]::ProtectData($plainbytes, [CryptoBase]::ReadSecureString("Password")) } static [byte[]] ProtectData([byte[]]$plainbytes, [string]$passw0rd) { return [CryptoBase]::ProtectData($plainbytes, $passw0rd, $null) } static [byte[]] ProtectData([byte[]]$plainbytes, [SecureString]$password) { return [CryptoBase]::ProtectData($plainbytes, $password, $null) } static [byte[]] ProtectData([byte[]]$plainbytes, [string]$passw0rd, [byte[]]$aad) { return [CryptoBase]::ProtectData($plainbytes, [xconvert]::ToSecurestring($passw0rd), $aad) } static [byte[]] ProtectData([byte[]]$plainbytes, [securestring]$password, [byte[]]$aad) { $salt = [byte[]]::new(16) $nonce = [byte[]]::new(12) [RandomNumberGenerator]::Fill($salt) [RandomNumberGenerator]::Fill($nonce) $passBytes = [Encoding]::UTF8.GetBytes([CryptoBase]::SecureStringToString($password)) $key = [Argon2id]::Hash($passBytes, $salt, 65536, 3, 4, 32) $ciphertext = [byte[]]::new($plainbytes.Length) $tag = [byte[]]::new(16) $aes = [System.Security.Cryptography.AesGcm]::new($key) try { $aes.Encrypt($nonce, $plainbytes, $ciphertext, $tag, $aad) } finally { $aes.Dispose() [Array]::Clear($passBytes, 0, $passBytes.Length) [Array]::Clear($key, 0, $key.Length) } # payload: version(1) + salt(16) + nonce(12) + tag(16) + ciphertext [byte[]]$payload = [byte[]]@(0x01) + $salt + $nonce + $tag + $ciphertext return $payload } static [byte[]] ProtectDataCascade([string]$plaintext) { return [CryptoBase]::ProtectDataCascade([Encoding]::UTF8.GetBytes($plaintext), [xconvert]::ToSecurestring([CryptoBase]::ReadSecureString("Password"))) } static [byte[]] ProtectDataCascade([byte[]]$plainbytes) { return [CryptoBase]::ProtectDataCascade($plainbytes, [xconvert]::ToSecurestring([CryptoBase]::ReadSecureString("Password"))) } static [byte[]] ProtectDataCascade([byte[]]$plainbytes, [securestring]$password) { $salt = [byte[]]::new(32) [RandomNumberGenerator]::Fill($salt) $passBytes = [Encoding]::UTF8.GetBytes([CryptoBase]::SecureStringToString($password)) $masterKey = [Argon2id]::Hash($passBytes, $salt, 65536, 4, 4, 64) [Array]::Clear($passBytes, 0, $passBytes.Length) $aesKey = [byte[]]$masterKey[0..31] $xchachaKey = [byte[]]$masterKey[32..63] [Array]::Clear($masterKey, 0, $masterKey.Length) $aesNonce = [byte[]]::new(12) [RandomNumberGenerator]::Fill($aesNonce) $innerCiphertext = [byte[]]::new($plainbytes.Length) $aesTag = [byte[]]::new(16) $aes = [System.Security.Cryptography.AesGcm]::new($aesKey) try { $aes.Encrypt($aesNonce, $plainbytes, $innerCiphertext, $aesTag) } finally { $aes.Dispose() [Array]::Clear($aesKey, 0, $aesKey.Length) } $innerPayload = [byte[]]$aesNonce + $aesTag + $innerCiphertext $xNonce = [byte[]]::new(24) [RandomNumberGenerator]::Fill($xNonce) $outerPayload = [XChaCha20Poly1305]::Encrypt($innerPayload, $xchachaKey, $xNonce) [Array]::Clear($xchachaKey, 0, $xchachaKey.Length) [byte[]]$res = [byte[]]@(0x02) + $salt + $xNonce + $outerPayload return $res } static [byte[]] UnprotectDataCascade([byte[]]$protectedBytes) { return [CryptoBase]::UnprotectDataCascade($protectedBytes, [xconvert]::ToSecurestring([CryptoBase]::ReadSecureString("Password"))) } static [byte[]] UnprotectDataCascade([byte[]]$protectedBytes, [securestring]$password) { if ($null -eq $protectedBytes -or $protectedBytes.Length -lt 57) { throw [ArgumentException]::new("Invalid cascade payload.") } if ($protectedBytes[0] -ne 0x02) { throw [ArgumentException]::new("Unsupported cascade payload version.") } $salt = [byte[]]$protectedBytes[1..32] $xNonce = [byte[]]$protectedBytes[33..56] $outerPayload = [byte[]]$protectedBytes[57..($protectedBytes.Length - 1)] $passBytes = [Encoding]::UTF8.GetBytes([CryptoBase]::SecureStringToString($password)) $masterKey = [Argon2id]::Hash($passBytes, $salt, 65536, 4, 4, 64) [Array]::Clear($passBytes, 0, $passBytes.Length) $aesKey = [byte[]]$masterKey[0..31] $xchachaKey = [byte[]]$masterKey[32..63] [Array]::Clear($masterKey, 0, $masterKey.Length) $innerPayload = [XChaCha20Poly1305]::Decrypt($outerPayload, $xchachaKey, $xNonce) [Array]::Clear($xchachaKey, 0, $xchachaKey.Length) $aesNonce = [byte[]]$innerPayload[0..11] $aesTag = [byte[]]$innerPayload[12..27] $innerCiphertext = [byte[]]$innerPayload[28..($innerPayload.Length - 1)] $plainbytes = [byte[]]::new($innerCiphertext.Length) $aes = [System.Security.Cryptography.AesGcm]::new($aesKey) try { $aes.Decrypt($aesNonce, $innerCiphertext, $aesTag, $plainbytes) return $plainbytes } finally { $aes.Dispose() [Array]::Clear($aesKey, 0, $aesKey.Length) } } static [byte[]] CreateSealedBox([byte[]]$plainbytes, [byte[]]$senderPrivateKey, [byte[]]$recipientPublicKey) { # NOTE: [Curve25519] currently wraps ECDH over NIST P-256 key material in this module. $sharedSecret = [Curve25519]::DeriveSharedSecret($senderPrivateKey, $recipientPublicKey) $info = [Encoding]::UTF8.GetBytes("CryptoBase_SealedBox_P256_v1") $symmetricKey = [HkdfCore]::DeriveKey($sharedSecret, $null, $info, 32) [Array]::Clear($sharedSecret, 0, $sharedSecret.Length) $nonce = [byte[]]::new(24) [RandomNumberGenerator]::Fill($nonce) $ciphertextWithTag = [XChaCha20Poly1305]::Encrypt($plainbytes, $symmetricKey, $nonce) [Array]::Clear($symmetricKey, 0, $symmetricKey.Length) return [byte[]]$nonce + $ciphertextWithTag } static [hashtable] ProtectDataQuantumHybrid([byte[]]$plainbytes, [byte[]]$recipientP256Pub, [byte[]]$recipientKemPub) { # NOTE: [Curve25519] currently wraps ECDH over NIST P-256 key material in this module. $ephemeralCurve = [Curve25519]::GenerateKeyPair() $classicShared = [Curve25519]::DeriveSharedSecret($ephemeralCurve.PrivateKey, $recipientP256Pub) $mlKem = [MLKemCore]::new() $kemResult = $mlKem.Encapsulate($recipientKemPub) $pqcShared = $kemResult.SharedSecret $hybridSecret = [BLAKE3]::ComputeHash([byte[]]$classicShared + $pqcShared) [Array]::Clear($classicShared, 0, $classicShared.Length) [Array]::Clear($pqcShared, 0, $pqcShared.Length) $nonce = [byte[]]::new(24) [RandomNumberGenerator]::Fill($nonce) $ciphertext = [XChaCha20Poly1305]::Encrypt($plainbytes, $hybridSecret, $nonce) [Array]::Clear($hybridSecret, 0, $hybridSecret.Length) return @{ Ciphertext = [byte[]]$nonce + $ciphertext EphemeralCurvePub = $ephemeralCurve.PublicKey KemCiphertext = $kemResult.Ciphertext } } static [byte[]] UnprotectData([byte[]]$protectedBytes) { return [CryptoBase]::UnprotectData($protectedBytes, [CryptoBase]::ReadSecureString("Password")) } static [byte[]] UnprotectData([byte[]]$protectedBytes, [string]$passw0rd) { return [CryptoBase]::UnprotectData($protectedBytes, [xconvert]::ToSecurestring($passw0rd), $null) } static [byte[]] UnprotectData([byte[]]$protectedBytes, [securestring]$password) { return [CryptoBase]::UnprotectData($protectedBytes, $password, $null) } static [byte[]] UnprotectData([byte[]]$protectedBytes, [string]$passw0rd, [byte[]]$aad) { if ([string]::IsNullOrWhiteSpace($passw0rd)) { throw [ArgumentException]::new("Password cannot be null or empty.") } return [CryptoBase]::UnprotectData($protectedBytes, [xconvert]::ToSecurestring($passw0rd), $aad) } static [byte[]] UnprotectData([byte[]]$protectedBytes, [securestring]$password, [byte[]]$aad) { if ($null -eq $protectedBytes -or $protectedBytes.Length -lt 45) { throw [ArgumentException]::new("Invalid protected payload.") } if ($protectedBytes[0] -ne 0x01) { throw [ArgumentException]::new("Unsupported payload version.") } $salt = [byte[]]$protectedBytes[1..16] $nonce = [byte[]]$protectedBytes[17..28] $tag = [byte[]]$protectedBytes[29..44] $ciphertext = [byte[]]$protectedBytes[45..($protectedBytes.Length - 1)] $passBytes = [Encoding]::UTF8.GetBytes([CryptoBase]::SecureStringToString($password)) $key = [Argon2id]::Hash($passBytes, $salt, 65536, 3, 4, 32) $plainbytes = [byte[]]::new($ciphertext.Length) $aes = [System.Security.Cryptography.AesGcm]::new($key) try { $aes.Decrypt($nonce, $ciphertext, $tag, $plainbytes, $aad) return $plainbytes } finally { $aes.Dispose() [Array]::Clear($passBytes, 0, $passBytes.Length) [Array]::Clear($key, 0, $key.Length) } } static [Secp256k1SignResult] SignMessage([string]$string) { return [CryptoBase]::SignMessage([Encoding]::UTF8.GetBytes($string)) } static [Secp256k1SignResult] SignMessage([byte[]]$data) { $kp = [Secp256k1]::GenerateKeyPair() $sig = [Secp256k1]::Sign($data, $kp.PrivateKey) return [Secp256k1SignResult]::new($sig, $kp.PrivateKey, $kp.PublicKey) } static [bool] VerifyMessage([string]$string, [byte[]]$signature, [byte[]]$publicKey) { return [CryptoBase]::VerifyMessage([Encoding]::UTF8.GetBytes($string), $signature, $publicKey) } static [bool] VerifyMessage([byte[]]$data, [byte[]]$signature, [byte[]]$publicKey) { return [Secp256k1]::Verify($data, $signature, $publicKey) } static [void] ObfuscateFile([string]$inputPath) { [CryptoBase]::ObfuscateFile($inputPath, "$inputPath.enc", [xconvert]::ToSecurestring([CryptoBase]::ReadSecureString("Password"))) } static [void] ObfuscateFile([string]$inputPath, [string]$outputPath, [securestring]$password) { $inputPath = [CryptoBase]::ResolvePath($inputPath) $outputPath = [CryptoBase]::ResolvePath($outputPath) if (![File]::Exists($inputPath)) { throw [FileNotFoundException]::new("Input file not found: $inputPath") } $data = [File]::ReadAllBytes($inputPath) $wrapped = [CryptoBase]::ProtectData($data, $password) $fileCrc = [Crc24]::ComputeToBytes($wrapped) [File]::WriteAllBytes($outputPath, $fileCrc + $wrapped) } static [void] DeobfuscateFile([string]$inputPath) { $outPath = $inputPath -replace '\.enc$', '' if ($outPath -eq $inputPath) { $outPath = "$inputPath.dec" } [CryptoBase]::DeobfuscateFile($inputPath, $outPath, [xconvert]::ToSecurestring([CryptoBase]::ReadSecureString("Password"))) } static [void] DeobfuscateFile([string]$inputPath, [string]$outputPath, [securestring]$password) { $inputPath = [CryptoBase]::ResolvePath($inputPath) $outputPath = [CryptoBase]::ResolvePath($outputPath) if (![File]::Exists($inputPath)) { throw [FileNotFoundException]::new("Input file not found: $inputPath") } $blob = [File]::ReadAllBytes($inputPath) if ($blob.Length -lt 4) { throw [ArgumentException]::new("Invalid obfuscated file.") } $crc = $blob[0..2] $payload = [byte[]]::new($blob.Length - 3) [Array]::Copy($blob, 3, $payload, 0, $payload.Length) if (![Crc24]::Verify($crc, $payload)) { throw [CryptographicException]::new("CRC24 verification failed.") } $plain = [CryptoBase]::UnprotectData($payload, $password) [File]::WriteAllBytes($outputPath, $plain) } static [string] ReadSecureString([string]$prompt) { if ([CryptoBase]::_SkipReadHostPrompts) { return [CryptoBase]::SecureStringToString([CryptoBase]::_Password) } [SecureString]$ss = Read-Host -Prompt $prompt -AsSecureString return [CryptoBase]::SecureStringToString($ss) } static [string] SecureStringToString([SecureString]$secureString) { [SecureString]$ss = $secureString.Copy(); $result = [string]::Empty $mdp = [Marshal]::SecureStringToBSTR($ss) try { $result = [Marshal]::PtrToStringBSTR($mdp) } finally { [Marshal]::ZeroFreeBSTR($mdp) $ss.Dispose() } return $result } static [string] ResolvePath([string]$path) { if ([string]::IsNullOrWhiteSpace($path)) { return $path } if ([Path]::IsPathRooted($path)) { return $path } try { $currentPath = (Get-Location).Path return [Path]::GetFullPath([Path]::Combine($currentPath, $path)) } catch { return [Path]::GetFullPath($path) } } } # Types that will be available to users when they import the module. # Hint: To automatically generate typestoexport variable you can use this one liner to generate types to export variable # (Get-ChildItem *.psm1 -Recurse -File | ForEach-Object { [IO.File]::ReadAllLines((Get-Item $_.FullName)).Where({ $_.StartsWith("class") -or $_.StartsWith("enum ") }).ForEach({ $_.Replace("class ", '[').Replace("enum ", '[') }).ForEach({ ($_ -like "* : *") ? $_.split(" : ")[0] + ']' : $_.Replace(' {', ']') }) }) -join ', ' $typestoExport = @( [AesCcmEncryptionResult], [AesCcmCore], [AesCcmBuilder], [AesCfb], [AesCmac], [AesCng], [AesCtr], [AesGCM], [AesOcbCore], [AesOcb], [AesSIV], [ArmorDecodeResult], [Armor], [BCryptCore], [BCrypt], [BCryptExtendedV3], [Blake2b], [ChaCha20Poly1305Managed], [Crc24], [CredManaged], [NativeCredential], [CredentialManager], [Curve25519], [Ecdsa], [Ed25519Impl], [Ed25519], [Ed448], [ECC], [EncryptionScope], [keyStoreMode], [KeyExportPolicy], [KeyProtection], [KeyUsage], [X509ContentType], [ECCurveName], [SdCategory], [ExpType], [CertStoreName], [CryptoAlgorithm], [RSAPadding], [Compression], [CredFlags], [CredType], [CredentialPersistence], [HashType], [AsymmetricAlgorithm], [KeyFormat], [KeySize], [ArmorType], [MLKemSecurityLevel], [SlhDsaSecurityLevel], [PgpHashAlgorithmId], [PgpPublicKeyAlgorithm], [PgpPacketTag], [PgpS2KUsage], [PgpCompressionAlgorithm], [PgpSignatureType], [PgpSignatureSubpacketType], [PgpUserAttributeSubpacketType], [PgpPacketFormat], [PgpLiteralDataFormat], [PgpImageEncoding], [PgpRevocationReason], [InvalidArgumentException], [CredentialNotFoundException], [IntegrityCheckFailedException], [InvalidPasswordException], [SaltParseException], [BcryptAuthenticationException], [HashInformationException], [KeypairException], [KeyGenerationException], [KeyImportException], [FileMonitor], [Hc128], [Hc256], [HkdfCore], [HkdfBuilder], [Keypair], [NamedKeypair], [KeypairGenerationResult], [KeypairHelper], [KeypairGen], [KeypairManager], [KMAC256], [MD5], [Expiration], [HashParser], [HashInformation], [HashFormatDescriptor], [CipherObject], [SecretStore], [KSFConfigType], [opaqueServerLoginState], [opaqueClientRegistrationState], [opaqueClientLoginState], [opaqueKSFConfig], [opaqueOpaqueServer], [opaqueOpaqueClient], [KSFConfig], [OpaqueServer], [OpaqueClient], [OPAQUE], [Mpi], [PgpPacketHeader], [PgpPublicKeyPacket], [PgpSecretKeyPacket], [PgpUserIdPacket], [PgpLiteralDataPacket], [OpenPgp], [OTPKIT], [Argon2id], [Argon2i], [Argon2d], [Scrypt], [Pbkdf2], [MLKemKeyPair], [MLKemEncapsulationResult], [MLKemCore], [MLKemBuilder], [MLDsaSecurityLevel], [MLDsaKeyPair], [MLDsaCore], [MLDsaBuilder], [SlhDsaKeyPair], [SlhDsaCore], [SlhDsaBuilder], [RabbitState], [Rabbit], [RSA], [S2KType], [PgpS2KSpecifier], [S2K], [Secp256k1], [Secp256k1SignResult], [Keccak], [KeccakManaged], [IdentityHash], [DoubleSha256], [SHA3256], [SHA3384], [SHA3512], [SHAKE128Managed], [SHAKE256Managed], [KMAC128], [FipsHmacSha256], [BLAKE3], [TripleDES], [Asn1Parser], [PemParser], [SecureBox], [SecureArray], [NoiseProtocol], [VOPRF], [BitwUtil], [Shuffl3r], [SignatureUtils], [CryptobaseUtils], [VaultClient], [X509], [XChaCha20Poly1305], [XOR], [XSalsa20], [CryptoBase] ) $TypeAcceleratorsClass = [PsObject].Assembly.GetType('System.Management.Automation.TypeAccelerators') # Add type accelerators for every exportable type. foreach ($Type in $typestoExport) { try { $TypeAcceleratorsClass::Add($Type.FullName, $Type) } catch { # Ignore if already exists $null } } # Remove type accelerators when the module is removed. $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { foreach ($Type in $typestoExport) { $TypeAcceleratorsClass::Remove($Type.FullName) } }.GetNewClosure(); $scripts = @(); $Public = Get-ChildItem "$PSScriptRoot/Public" -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue $scripts += Get-ChildItem "$PSScriptRoot/Private" -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue $scripts += $Public foreach ($file in $scripts) { try { if ([string]::IsNullOrWhiteSpace($file.fullname)) { continue } . "$($file.fullname)" } catch { Write-Warning "Failed to import function $($file.BaseName): $_" $host.UI.WriteErrorLine($_) } } $Param = @{ Function = $Public.BaseName Cmdlet = '*' Alias = '*' Verbose = $false } Export-ModuleMember @Param |