MS DocumentViewer

This commit is contained in:
Developer01
2025-11-28 09:23:48 +01:00
parent b87995221f
commit 07332a4990
17 changed files with 838 additions and 181 deletions

View File

@@ -226,6 +226,7 @@
<Compile Include="Scheduler\DatatableJob.vb" />
<Compile Include="Scheduler\JobListener.vb" />
<Compile Include="Scheduler\JobResult.vb" />
<Compile Include="Security\SecureFileHandler.vb" />
<Compile Include="ServiceHost.vb" />
<Compile Include="WindowsService.vb">
<SubType>Component</SubType>

View File

@@ -2,6 +2,7 @@
Imports DigitalData.Modules.Logging
Imports DigitalData.Modules.Language
Imports System.IO
Imports DigitalData.Services.EDMIService.Security
Namespace Methods.IDB.GetFileObject
Public Class GetFileObjectMethod
@@ -69,6 +70,29 @@ Namespace Methods.IDB.GetFileObject
End Function
Private Function LoadFileContents(pFilePath As String) As Byte()
Try
Dim password = Environment.GetEnvironmentVariable("DD_FILE_ENCRYPTION_PASSWORD")
If String.IsNullOrWhiteSpace(password) Then
Logger.Warn("No encryption password set (DD_FILE_ENCRYPTION_PASSWORD). Attempting legacy plain read for file [{0}]", pFilePath)
Return ReadPlain(pFilePath)
End If
Try
' Try decrypt first (preferred path)
Logger.Debug("Attempting AES decrypt for file [{0}]", pFilePath)
Return SecureFileHandler.DecryptFileToBytes(pFilePath, password)
Catch exDec As Exception
Logger.Warn("Decrypt failed for file [{0}]. Falling back to plain read. Reason: {1}", pFilePath, exDec.Message)
Logger.Error(exDec)
Return ReadPlain(pFilePath)
End Try
Catch ex As Exception
Logger.Error(ex)
Return Nothing
End Try
End Function
Private Function LoadFileContents_Old(pFilePath As String) As Byte()
Try
Using oFileStream As New FileStream(pFilePath, FileMode.Open, FileAccess.Read)
Using oMemoryStream As New MemoryStream()
@@ -85,6 +109,15 @@ Namespace Methods.IDB.GetFileObject
End Try
End Function
Private Function ReadPlain(pFilePath As String) As Byte()
Using oFileStream As New FileStream(pFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)
Using oMemoryStream As New MemoryStream()
oFileStream.CopyTo(oMemoryStream)
Return oMemoryStream.ToArray()
End Using
End Using
End Function
End Class
End Namespace

View File

@@ -3,6 +3,7 @@ Imports DigitalData.Modules.Base.IDB.Constants
Imports DigitalData.Modules.Database
Imports DigitalData.Modules.Database.MSSQLServer.TransactionMode
Imports DigitalData.Modules.Logging
Imports DigitalData.Services.EDMIService.Security
Namespace Methods.IDB.NewFile
Public Class NewFileMethod
@@ -22,6 +23,197 @@ Namespace Methods.IDB.NewFile
Logger.Debug("Running [NewFileMethod].")
Dim oFilePath As String = Nothing
Try
If pData.File Is Nothing Then
Throw New ArgumentNullException(NameOf(pData.File))
End If
If pData.KindType Is Nothing Then
Throw New ArgumentNullException(NameOf(pData.KindType))
End If
If pData.StoreName Is Nothing Then
Throw New ArgumentNullException(NameOf(pData.StoreName))
End If
If pData.User Is Nothing Then
Throw New ArgumentNullException(NameOf(pData.User))
End If
If IsNothing(pData.IDBDoctypeId) Then
Throw New ArgumentNullException(NameOf(pData.IDBDoctypeId))
End If
Logger.Debug("Checking if checksum already exists..")
Dim oExistingObjectId = Helpers.TestFileChecksumExists(pData.File.FileChecksum)
If oExistingObjectId > 0 Then
Return New NewFileResponse(oExistingObjectId)
End If
Logger.Debug("Creating New ObjectId..")
Dim oObjectId = Helpers.NewObjectIdWithTransaction(pData.KindType, pData.User.UserName, Connection, Transaction)
If oObjectId = 0 Then
LogAndThrow("Could not create new ObjectId!")
End If
Logger.Debug("New ObjectId [{0}] created!", oObjectId)
' Find ObjectStore by Title
Logger.Debug("Checking for DataStore [{0}].", pData.StoreName)
Dim oStore = GlobalState.ObjectStores.
Where(Function(store) store.Title = pData.StoreName).
SingleOrDefault()
If oStore Is Nothing Then
LogAndThrow($"DataStore [{pData.StoreName}] does not exist. Exiting.")
End If
Logger.Debug("Using DataStore [{0}].", pData.StoreName)
' Get Store base and final path
Logger.Debug("Store BasePath is [{0}]", oStore.Path)
Dim oFinalPath = Helpers.GetFileObjectPath(oStore, pData.File.FileImportedAt)
' Ensure target directory exists
Try
If Not IO.Directory.Exists(oFinalPath) Then
IO.Directory.CreateDirectory(oFinalPath)
End If
Catch exDir As Exception
LogAndThrow(exDir, $"Target directory [{oFinalPath}] could not be created.")
End Try
' Get filename
Dim oKeepFileName As Boolean = False
If oStore.IsArchive Then
Logger.Debug("Object Store is an archive: [{0}]", oStore.IsArchive)
oKeepFileName = True
End If
Dim oFileName As String = GetFileObjectFileName(oObjectId, pData.File.FileName, oKeepFileName)
Logger.Debug("Filename is [{0}]", oFileName)
oFilePath = IO.Path.Combine(oFinalPath, oFileName)
Dim oFileObjectInfo As IO.FileInfo = New IO.FileInfo(oFilePath)
Dim oFileObjectName As String = oFileObjectInfo.Name
Logger.Debug("File Information for [{0}]:", oFileObjectName)
Dim oFileObjectSize As Long = pData.File.FileContents.Length ' original (plaintext) size
Logger.Debug("Original Size: [{0}]", oFileObjectSize)
Dim oOriginalExtension As String = pData.File.FileInfoRaw.Extension.Substring(1)
Logger.Debug("Original Extension: [{0}]", oOriginalExtension)
Logger.Debug("Checksum: [{0}]", pData.File.FileChecksum)
' Retrieve encryption password (environment variable)
Dim encryptionPassword As String = Environment.GetEnvironmentVariable("DD_FILE_ENCRYPTION_PASSWORD")
If String.IsNullOrWhiteSpace(encryptionPassword) Then
LogAndThrow("Encryption password not configured (env DD_FILE_ENCRYPTION_PASSWORD).")
End If
' Perform encryption with strict failure handling
Try
Logger.Info("Encrypting and saving file to path [{0}]", oFilePath)
SecureFileHandler.EncryptFileFromBytes(pData.File.FileContents, oFilePath, encryptionPassword)
Catch exEnc As Exception
LogAndThrow(exEnc, $"Could not encrypt/write file [{oFilePath}] to disk!")
End Try
' Post-encryption validation: file must exist and contain at least header bytes
Try
Dim fi As New IO.FileInfo(oFilePath)
If Not fi.Exists Then
LogAndThrow($"Encrypted file was not created at [{oFilePath}].")
End If
' Minimum file size:1 (version) +4 (iterations) +32 (salt) =37 bytes
If fi.Length < 37 Then
LogAndThrow($"Encrypted file at [{oFilePath}] is invalid or truncated (size {fi.Length}).")
End If
Logger.Debug("Encrypted physical file size: [{0}]", fi.Length)
Catch exVal As Exception
' LogAndThrow above will throw; any other IO errors should also abort here
LogAndThrow(exVal, "Encrypted file validation failed.")
End Try
'---------------------------------------------------------------------------
Logger.Info("Creating IDB FileObject for ObjectId [{0}].", oObjectId)
' Insert into DB (store original plaintext size for consistency)
Dim oSQL As String = $"EXEC PRIDB_NEW_IDBFO
'{oFinalPath}',
'{oFileObjectName}',
'{oOriginalExtension}',
{oFileObjectSize},
'{pData.File.FileChecksum}' ,
'{pData.User.UserName}',
'{oObjectId}',
{oStore.Id},
{pData.IDBDoctypeId}"
Dim oResult As Boolean = DatabaseIDB.ExecuteNonQueryWithConnectionObject(oSQL, Connection, ExternalTransaction, Transaction)
If oResult = False Then
LogAndThrow("IDB FileObject could not be created!")
End If
'---------------------------------------------------------------------------
Dim oSystemAttributes As New Dictionary(Of String, Object) From {
{Attributes.ATTRIBUTE_ORIGIN_FILENAME, pData.File.FileName},
{Attributes.ATTRIBUTE_ORIGIN_CREATED, pData.File.FileCreatedAt},
{Attributes.ATTRIBUTE_ORIGIN_CHANGED, pData.File.FileChangedAt}
}
For Each oAttribute As KeyValuePair(Of String, Object) In oSystemAttributes
Try
' Dont write empty attributes
If oAttribute.Value Is Nothing Then
Continue For
End If
Dim oSuccess = Helpers.SetAttributeValueWithTransaction(Connection, Transaction, oObjectId, oAttribute.Key, oAttribute.Value, pData.User.Language, pData.User.UserName)
If oSuccess Then
Logger.Debug("System Attribute [{0}] written with value [{1}]", oAttribute.Key, oAttribute.Value)
Else
Logger.Warn("System attribute value could not be written")
End If
Catch ex As Exception
LogAndThrow(ex, $"System attribute [{oAttribute.Key}] could not be written!")
End Try
Next
'---------------------------------------------------------------------------
' Finally, commit the transaction
Transaction?.Commit()
Return New NewFileResponse(oObjectId)
Catch ex As Exception
Logger.Warn("Error occurred while creating file!")
Logger.Error(ex)
Logger.Info("Cleaning up files.")
If Not IsNothing(oFilePath) AndAlso IO.File.Exists(oFilePath) Then
Try
IO.File.Delete(oFilePath)
Catch exInner As Exception
Logger.Warn("Error while cleaning up files.")
Logger.Error(exInner)
End Try
End If
Logger.Info("Rolling back transaction.")
Transaction?.Rollback()
Return New NewFileResponse(ex)
End Try
End Function
Public Function Run_Old(pData As NewFileRequest) As NewFileResponse
Logger.Debug("Running [NewFileMethod Old].")
Dim oFilePath As String = Nothing
Try
If pData.File Is Nothing Then
Throw New ArgumentNullException(NameOf(pData.File))
@@ -181,7 +373,6 @@ Namespace Methods.IDB.NewFile
End Try
End Function
Private Function GetFileObjectFileName(IDB_OBJ_ID As Long, pFilename As String, pKeepFilename As Boolean) As String
' TODO: save actual extensions
If pKeepFilename Then

View File

@@ -0,0 +1,146 @@
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