Last active
December 10, 2024 16:14
-
-
Save PanosGreg/c547fb244607b85a3e06431173bcaae0 to your computer and use it in GitHub Desktop.
Encryption-Decryption with and without .NET streams & Key Generation with PBKDF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# I wrote these notes in Sep-2024 | |
# AES 128 and 256 bits | |
# Without using .NET Streams | |
## Initial Input | |
[string]$PlainText = 'this is a test message' | |
[string]$Password = 'SampleSecret' | |
[ValidateSet('AES128','AES256')] | |
[string]$AesAlgoName = 'AES128' | |
[ValidateSet('SHA256','SHA384','SHA512')] | |
[string]$HashAlgoName = 'SHA256' | |
[ValidateRange(10,31)] # <-- the limits are the same as bcrypt | |
$HashWorkFactor = 20 # <-- default is 2^20 = 1.048.576 iterations | |
## Base variables | |
$PlainBytes = [System.Text.Encoding]::ASCII.GetBytes($PlainText) | |
switch ($AesAlgoName) { | |
'AES128' {$AesKeySize = 128 ; $AesKeyLength = 16} | |
'AES256' {$AesKeySize = 256 ; $AesKeyLength = 32} | |
} | |
switch ($HashAlgoName) { | |
'SHA256' {$HashAlgoLength = 32 ; $KeyHashAlgoName = 'HMACSHA256'} | |
'SHA384' {$HashAlgoLength = 48 ; $KeyHashAlgoName = 'HMACSHA384'} | |
'SHA512' {$HashAlgoLength = 64 ; $KeyHashAlgoName = 'HMACSHA512'} | |
} | |
## Create Master Key, Initialization Vector and HMAC Key | |
$PassBytes = [System.Text.Encoding]::UTF8.GetBytes($Password) | |
$Salt = [System.Security.Cryptography.RandomNumberGenerator]::GetBytes($HashAlgoLength) | |
$HashAlgo = [System.Security.Cryptography.HashAlgorithmName]::$HashAlgoName | |
$Iterations = [math]::Pow(2,$HashWorkFactor) | |
$PBKDF = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($PassBytes,$Salt,$Iterations,$HashAlgo) | |
$AesKey = $PBKDF.GetBytes($AesKeyLength) | |
$AesIV = $PBKDF.GetBytes(16) | |
$MacKey = $PBKDF.GetBytes($HashAlgoLength) | |
$PBKDF.Dispose() | |
# PBKDF = Password-Based Key-Derivation Function | |
## ENCRYPT | |
$Aes = [System.Security.Cryptography.Aes]::Create() | |
$Aes.Mode = [System.Security.Cryptography.CipherMode]::CBC | |
$Aes.Padding = [System.Security.Cryptography.PaddingMode]::ISO10126 | |
$Aes.BlockSize = 128 | |
$Aes.KeySize = $AesKeySize | |
$Encryptor = $Aes.CreateEncryptor($AesKey,$AesIV) | |
$CipherBytes = $Encryptor.TransformFinalBlock($PlainBytes, 0, $PlainBytes.Length) | |
$CipherBase64 = [System.Convert]::ToBase64String($CipherBytes) | |
$Aes.Dispose() | |
Write-Output $CipherBase64 | |
## DECRYPT | |
$Aes = [System.Security.Cryptography.Aes]::Create() | |
$Aes.Mode = [System.Security.Cryptography.CipherMode]::CBC | |
$Aes.Padding = [System.Security.Cryptography.PaddingMode]::ISO10126 | |
$Aes.BlockSize = 128 | |
$Aes.KeySize = $AesKeySize | |
$Decryptor = $Aes.CreateDecryptor($AesKey,$AesIV) | |
$PlainBytes2 = $Decryptor.TransformFinalBlock($CipherBytes, 0, $CipherBytes.Length) | |
$PlainText2 = [Text.Encoding]::UTF8.GetString($PlainBytes2) | |
$Aes.Dispose() | |
Write-Output $PlainText2 | |
## Calculate MAC | |
$Sha = Invoke-Expression -Command "[System.Security.Cryptography.$HashAlgoName]::Create()" | |
$Hmac = Invoke-Expression -Command "[System.Security.Cryptography.$KeyHashAlgoName]::Create()" | |
$Hmac.Key = $MacKey | |
$PlainHash = $Sha.ComputeHash($PlainBytes) | |
$PlainHash2 = $Sha.ComputeHash($PlainBytes2) | |
$SecureHash = $Hmac.ComputeHash($PlainBytes) | |
$SecureHash2 = $Hmac.ComputeHash($PlainBytes2) | |
## Confirm MAC | |
[System.Linq.Enumerable]::SequenceEqual($PlainHash,$PlainHash2) | |
[System.Linq.Enumerable]::SequenceEqual($SecureHash,$SecureHash2) | |
# PBKDF2 = Password-Based Key-Derivation Function 2 | |
# The Create method with a string on the hash classes is obsolete | |
# for ex. [System.Security.Cryptography.HashAlgorithm]::Create('SHA256') <-- this is obsolete | |
# So you have to use the specific class for the Hash algo that you are using | |
# Which means we were going to use New-Object to create a new instance | |
# but since there is no constructor (as-in ::new() ), but only the Create() method | |
# that leaves us with either [scriptblock]::Create('our string') or Invoke-Expression | |
# First get the byte array for the salt via the RNG (random number generator) | |
# This is 32 bytes long for SHA256 on the PBKDF2 | |
# Get our All the needed random bytes arrays from PBKDF2 | |
# These are the AES Key (32), IV (16) and HMAC Key (32 for SHA256) from PBKDF2 | |
# Agree on the MAC algo at the beginning of the process | |
# And use the same on for all operations. | |
# For ex. choose 256, 384 or 512 for SHA or HMAC | |
# and then use it in: | |
# - PBKDF2 to generate the master key, iv, hmac key | |
# - SHA or HMAC to calculate the MAC of a string for integrity check | |
### General Notes | |
# 1) FIPS Compliance | |
# By using the [System.Security.Cryptography.Aes] class | |
# we are FIPS-140-2 Compliant | |
# 2) Key Generation | |
# By using the [System.Security.Cryptography.Rfc2898DeriveBytes] class | |
# (along with a proper iteration number and a compliant hash algorithm) | |
# we are generating a properly secure encryption key | |
# For the number of iterations I'm using 1 million. | |
# Based on the OWASP recommendation as of Dec-2022, we should be using | |
# at least 600.000 iterations with SHA256 | |
# Link: https://github.com/OWASP/CheatSheetSeries/blob/20750a1f1887de50ed444d424f252e472f02ca8b/cheatsheets/Password_Storage_Cheat_Sheet.md | |
# https://tobtu.com/minimum-password-settings/ | |
# 3) RNG | |
# By using the [System.Security.Cryptography.RandomNumberGenerator] class | |
# we generate cryptographically proper random numbers | |
# 4) Stream processing | |
# The above example does NOT use streaming processing via .NET stream-based classes | |
# I'll include a separate example using streams | |
# 5) Padding mode | |
# I'm using the padding mode ISO10126 which is better then the default PKCS7 | |
# based on MS recommendation | |
# https://learn.microsoft.com/en-us/dotnet/standard/security/vulnerabilities-cbc-mode | |
# Padding Modes reference: | |
# https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.paddingmode?view=net-8.0#fields | |
# 6) Authentication order | |
# When encrypting we must first encrypt the plain text and then hash the ciphertext. | |
# Similarly during decryption, we must first authenticate the hash, and then decrypt the ciphertext | |
# This pattern is called "encrypt-then-sign". | |
# https://learn.microsoft.com/en-us/dotnet/standard/security/vulnerabilities-cbc-mode | |
# Q&A | |
# 1) Q: Why do we specify the AES properties, even though the same are the defaults? | |
# For example the default AES block size is 128 and the default mode is CBC | |
# But we still specify it in the code. | |
# A: 2 reasons for that: | |
# a) in case there is a change in the .net framework in a future version where these | |
# defaults get changed. We won't be affected as we explicitly specify them. | |
# b) for clarity so the end-user who reads the code, knows from the get-go what we | |
# are using for the encryption. | |
# 2) Q: Why did I choose to name the variable for PBKDF as "WorkFactor" instead of "Iterations" | |
# The constructor from the .NET PBKDF class (Rfc2898DeriveBytes) has the number of | |
# iterations after all, not work factor. | |
# A: The work factor name is used by the bCrypt algorithm. | |
# Since now (from Nov-2022 onwards) the recommended number of iterations is in the hundreds | |
# of thousands, it made sense for me to put it as a power of two (like in bcrypt). | |
# Instead of having a huge number as input (for ex. 600.000 or 1.000.000) | |
# You see it's also a psychologic thing for the end-user, cause if it's say 50.000 and you tell | |
# him we need more, then he'll go for example 80K or 100K, but won't go 500K. Whereas with the | |
# work factor, if you increase the number from say 15 to 20, it's only like 5 numbers, but you | |
# effectively increase it from 32K to 1M, which is huge. | |
# a link | |
# https://gist.github.com/geoffgarside/c28816a48516794095b96dcc5944ad25 | |
## =========================== | |
## Encryption - Decryption with Streams (for both encryption/decryption and encoding/decoding) | |
# Note: on the following example I have not included compression into the process. | |
# gzip compression & decompression can be added as an extra stream. | |
## Initial Input | |
[string]$PlainText = 'this is a test message' | |
[string]$Password = 'SampleSecret' | |
[ValidateSet('AES128','AES256')] | |
[string]$AesAlgoName = 'AES128' | |
[ValidateSet('SHA256','SHA384','SHA512')] | |
[string]$HashAlgoName = 'SHA256' | |
[ValidateRange(10,31)] # <-- the limits are the same as bcrypt | |
$HashWorkFactor = 20 # <-- default is 2^20 = 1.048.576 iterations | |
## Base variables | |
$PlainBytes = [System.Text.Encoding]::ASCII.GetBytes($PlainText) | |
switch ($AesAlgoName) { | |
'AES128' {$AesKeySize = 128 ; $AesKeyLength = 16} | |
'AES256' {$AesKeySize = 256 ; $AesKeyLength = 32} | |
} | |
switch ($HashAlgoName) { | |
'SHA256' {$HashAlgoLength = 32 ; $KeyHashAlgoName = 'HMACSHA256'} | |
'SHA384' {$HashAlgoLength = 48 ; $KeyHashAlgoName = 'HMACSHA384'} | |
'SHA512' {$HashAlgoLength = 64 ; $KeyHashAlgoName = 'HMACSHA512'} | |
} | |
## Create Master Key, Initialization Vector and HMAC Key | |
$PassBytes = [System.Text.Encoding]::UTF8.GetBytes($Password) | |
$Salt = [System.Security.Cryptography.RandomNumberGenerator]::GetBytes($HashAlgoLength) | |
$HashAlgo = [System.Security.Cryptography.HashAlgorithmName]::$HashAlgoName | |
$Iterations = [math]::Pow(2,$HashWorkFactor) | |
$PBKDF = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($PassBytes,$Salt,$Iterations,$HashAlgo) | |
$AesKey = $PBKDF.GetBytes($AesKeyLength) | |
$AesIV = $PBKDF.GetBytes(16) | |
$MacKey = $PBKDF.GetBytes($HashAlgoLength) | |
$PBKDF.Dispose() | |
## ENCRYPT (the input is $PlainText) | |
$Aes = [System.Security.Cryptography.Aes]::Create() | |
$Aes.Mode = [System.Security.Cryptography.CipherMode]::CBC | |
$Aes.Padding = [System.Security.Cryptography.PaddingMode]::ISO10126 | |
$Aes.BlockSize = 128 | |
$Aes.KeySize = $AesKeySize | |
$Encryptor = $Aes.CreateEncryptor($AesKey,$AesIV) | |
$Encoder = [System.Security.Cryptography.ToBase64Transform]::new() | |
$MemoryStream = [System.IO.MemoryStream]::new() | |
$CryptoMode = [System.Security.Cryptography.CryptoStreamMode]::Write | |
$EncodeStream = [System.Security.Cryptography.CryptoStream]::new($MemoryStream,$Encoder,$CryptoMode) | |
$EncryptStream = [System.Security.Cryptography.CryptoStream]::new($EncodeStream,$Encryptor,$CryptoMode) | |
$WriteStream = [System.IO.StreamWriter]::new($EncryptStream) | |
$WriteStream.Write($PlainText) | |
# The order of dispose matters | |
$WriteStream.Dispose() | |
$MemoryStream.Dispose() | |
$Aes.Dispose() | |
$Encryptor.Dispose() | |
$Encoder.Dispose() | |
$EncryptStream.Dispose() | |
$EncodeStream.Dispose() | |
$CipherBytes = $MemoryStream.ToArray() | |
$CipherBase64 = [System.Text.Encoding]::ASCII.GetString($CipherBytes) | |
Write-Output $CipherBase64 | |
## DECRYPT (the input is $CipherBytes) | |
$Aes = [System.Security.Cryptography.Aes]::Create() | |
$Aes.Mode = [System.Security.Cryptography.CipherMode]::CBC | |
$Aes.Padding = [System.Security.Cryptography.PaddingMode]::ISO10126 | |
$Aes.BlockSize = 128 | |
$Aes.KeySize = $AesKeySize | |
$Decryptor = $Aes.CreateDecryptor($AesKey,$AesIV) | |
$Decoder = [System.Security.Cryptography.FromBase64Transform]::new() | |
$MemoryStream = [System.IO.MemoryStream]::new($CipherBytes) | |
$CryptoMode = [System.Security.Cryptography.CryptoStreamMode]::Read | |
$DecodeStream = [System.Security.Cryptography.CryptoStream]::new($MemoryStream,$Decoder,$CryptoMode) | |
$DecryptStream = [System.Security.Cryptography.CryptoStream]::new($DecodeStream,$Decryptor,$CryptoMode) | |
$ReadStream = [System.IO.StreamReader]::new($DecryptStream) | |
$PlainText2 = $ReadStream.ReadToEnd() | |
# The order of dispose matters | |
$MemoryStream.Dispose() | |
$DecryptStream.Dispose() | |
$DecodeStream.Dispose() | |
$ReadStream.Dispose() | |
$Aes.Dispose() | |
$Decryptor.Dispose() | |
$Decoder.Dispose() | |
Write-Output $PlainText2 | |
## ===================== | |
## Consider using bcrypt for hashing instead of SHA,SHA2 or even SHA3 (only available on Win2022+) | |
## bCrypt is considered more secure than SHA/SHA2/SHA3 | |
# Articles: | |
# https://codahale.com/how-to-safely-store-a-password/ | |
# https://tobtu.com/minimum-password-settings/ | |
# https://anthonysimmon.com/evolutive-and-robust-password-hashing-using-pbkdf2-in-dotnet/ | |
# https://github.com/OWASP/CheatSheetSeries/blob/20750a1f1887de50ed444d424f252e472f02ca8b/cheatsheets/Password_Storage_Cheat_Sheet.md | |
# https://tobtu.com/minimum-password-settings/ | |
# Minimum Hash Iterations for PBKDF2 | |
# Date: Dec-2022 | |
# SHA256: 600.000 | |
# SHA512: 210.000 | |
# Source: https://tobtu.com/minimum-password-settings/ | |
### ---- | |
# In the future I may need to make changes to this code, to improve security | |
# once new classes are available in .NET. Specifically: | |
# 1) Change hashing algo of PBKDF from SHA to bCrypt | |
# bCrypt is currently available through the external library BCrypt.Net-Next | |
# https://www.nuget.org/packages/BCrypt.Net-Next | |
# 2) change encryption mode from CBC to GCM (or CCM) | |
# GCM is currently available through the external library Bouncy Castle | |
# https://www.nuget.org/packages/BouncyCastle.Cryptography | |
# https://www.bouncycastle.org/ | |
# 3) Change the Key-less MAC from SHA to SHA3 | |
# SHA3 is available on Windows Server 2022 and later | |
# SHA (SHA256,SHA512) is fast and thus it is good for hashing a lot of data, like large files | |
# bCrypt is not as fast and it is good for hashing small data like passwords | |
# about HKDF | |
# we could change the PBKDF library from PBKDF2 to HKDF | |
# HKDF is available from .NET Core 5+, but not on .NET Framework | |
# HKDF is also available through the external library Bouncy Castle | |
# https://anthonysimmon.com/key-derivation-dotnet-using-hkdf/ | |
# It is advisable to use PBKDF2 and not HKDF for password storage though. | |
# https://stackoverflow.com/questions/28798292/hkdf-or-pbkdf2-for-generating-key-for-symmetric-encryption-python-cryptography |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment