Monorepo/Service.EDMIService/Security/SecureFileHandler.vb
2025-11-28 09:23:48 +01:00

147 lines
7.1 KiB
VB.net

Imports System.IO
Imports System.Security.Cryptography
Namespace Security
''' <summary>
''' Provides secure AES file encryption and decryption using PBKDF2 (Rfc2898DeriveBytes) for key derivation.
''' File format:
''' [1 byte Version][4 bytes IterationCount (Int32, big-endian)][32 bytes Salt][Encrypted Payload]
''' </summary>
Public NotInheritable Class SecureFileHandler
Private Sub New()
End Sub
Private Const CURRENT_VERSION As Byte = 1
Private Const SALT_LENGTH As Integer = 32
Private Const KEY_SIZE_BYTES As Integer = 32 ' AES-256
Private Const IV_SIZE_BYTES As Integer = 16 ' AES Block size (128 bit)
Private Const DEFAULT_ITERATIONS As Integer = 100000
Private Const BUFFER_SIZE As Integer = 81920 '80KB streaming buffer
''' <summary>
''' Encrypts the provided byte array and writes an encrypted file to the target path using streaming.
''' </summary>
Public Shared Sub EncryptFileFromBytes(sourceData() As Byte, targetFilePath As String, password As String, Optional iterations As Integer = DEFAULT_ITERATIONS)
If sourceData Is Nothing Then Throw New ArgumentNullException(NameOf(sourceData))
If String.IsNullOrWhiteSpace(password) Then Throw New ArgumentNullException(NameOf(password))
Using fsOut = New FileStream(targetFilePath, FileMode.Create, FileAccess.Write, FileShare.None)
Dim salt = GenerateRandomBytes(SALT_LENGTH)
' Write header: Version, Iterations, Salt
fsOut.WriteByte(CURRENT_VERSION)
WriteInt32BigEndian(fsOut, iterations)
fsOut.Write(salt, 0, salt.Length)
Using keyDerivation = New Rfc2898DeriveBytes(password, salt, iterations)
Dim key = keyDerivation.GetBytes(KEY_SIZE_BYTES)
Dim iv = keyDerivation.GetBytes(IV_SIZE_BYTES)
Dim aesAlg As System.Security.Cryptography.Aes = System.Security.Cryptography.Aes.Create()
Try
aesAlg.KeySize = KEY_SIZE_BYTES * 8
aesAlg.BlockSize = IV_SIZE_BYTES * 8
aesAlg.Mode = CipherMode.CBC
aesAlg.Padding = PaddingMode.PKCS7
aesAlg.Key = key
aesAlg.IV = iv
Using crypto = aesAlg.CreateEncryptor()
Using cs = New CryptoStream(fsOut, crypto, CryptoStreamMode.Write)
Using msIn = New MemoryStream(sourceData, writable:=False)
Dim buffer(BUFFER_SIZE - 1) As Byte
Dim read As Integer
Do
read = msIn.Read(buffer, 0, buffer.Length)
If read <= 0 Then Exit Do
cs.Write(buffer, 0, read)
Loop
End Using
cs.FlushFinalBlock()
End Using
End Using
Finally
aesAlg.Dispose()
End Try
End Using
End Using
End Sub
''' <summary>
''' Decrypts the encrypted file and returns the plaintext bytes.
''' </summary>
Public Shared Function DecryptFileToBytes(encryptedFilePath As String, password As String) As Byte()
If String.IsNullOrWhiteSpace(encryptedFilePath) Then Throw New ArgumentNullException(NameOf(encryptedFilePath))
If String.IsNullOrWhiteSpace(password) Then Throw New ArgumentNullException(NameOf(password))
Using fsIn = New FileStream(encryptedFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)
Dim version = CByte(fsIn.ReadByte())
If version <> CURRENT_VERSION Then Throw New InvalidDataException("Unsupported file version.")
Dim iterations = ReadInt32BigEndian(fsIn)
Dim salt = New Byte(SALT_LENGTH - 1) {}
ReadExact(fsIn, salt, 0, salt.Length)
Using keyDerivation = New Rfc2898DeriveBytes(password, salt, iterations)
Dim key = keyDerivation.GetBytes(KEY_SIZE_BYTES)
Dim iv = keyDerivation.GetBytes(IV_SIZE_BYTES)
Dim aesAlg As System.Security.Cryptography.Aes = System.Security.Cryptography.Aes.Create()
Try
aesAlg.KeySize = KEY_SIZE_BYTES * 8
aesAlg.BlockSize = IV_SIZE_BYTES * 8
aesAlg.Mode = CipherMode.CBC
aesAlg.Padding = PaddingMode.PKCS7
aesAlg.Key = key
aesAlg.IV = iv
Using crypto = aesAlg.CreateDecryptor()
Using cs = New CryptoStream(fsIn, crypto, CryptoStreamMode.Read)
Using msOut = New MemoryStream()
Dim buffer(BUFFER_SIZE - 1) As Byte
Dim read As Integer
Do
read = cs.Read(buffer, 0, buffer.Length)
If read <= 0 Then Exit Do
msOut.Write(buffer, 0, read)
Loop
Return msOut.ToArray()
End Using
End Using
End Using
Finally
aesAlg.Dispose()
End Try
End Using
End Using
End Function
Private Shared Function GenerateRandomBytes(length As Integer) As Byte()
Dim data = New Byte(length - 1) {}
Using rng = RandomNumberGenerator.Create()
rng.GetBytes(data)
End Using
Return data
End Function
Private Shared Sub WriteInt32BigEndian(stream As Stream, value As Integer)
Dim bytes = BitConverter.GetBytes(value)
If BitConverter.IsLittleEndian Then Array.Reverse(bytes)
stream.Write(bytes, 0, bytes.Length)
End Sub
Private Shared Function ReadInt32BigEndian(stream As Stream) As Integer
Dim bytes = New Byte(3) {}
ReadExact(stream, bytes, 0, 4)
If BitConverter.IsLittleEndian Then Array.Reverse(bytes)
Return BitConverter.ToInt32(bytes, 0)
End Function
Private Shared Sub ReadExact(stream As Stream, buffer As Byte(), offset As Integer, count As Integer)
Dim totalRead As Integer = 0
While totalRead < count
Dim read = stream.Read(buffer, offset + totalRead, count - totalRead)
If read <= 0 Then Throw New EndOfStreamException("Unexpected end of stream.")
totalRead += read
End While
End Sub
End Class
End Namespace