Skip to content

Instantly share code, notes, and snippets.

@PanosGreg
Last active December 10, 2024 16:14
Show Gist options
  • Save PanosGreg/c547fb244607b85a3e06431173bcaae0 to your computer and use it in GitHub Desktop.
Save PanosGreg/c547fb244607b85a3e06431173bcaae0 to your computer and use it in GitHub Desktop.
Encryption-Decryption with and without .NET streams & Key Generation with PBKDF
# 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