MS DocumentViewer
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
146
Service.EDMIService/Security/SecureFileHandler.vb
Normal file
146
Service.EDMIService/Security/SecureFileHandler.vb
Normal 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
|
||||
Reference in New Issue
Block a user