jj: FileContainer Version 0.0.2

This commit is contained in:
Jonathan Jenne 2018-11-21 17:03:05 +01:00
parent 0f105b6ebf
commit 7b2ae3abc3
6 changed files with 190 additions and 241 deletions

View File

@ -3,7 +3,7 @@ Imports System.IO
Imports Microsoft.VisualStudio.TestTools.UnitTesting
Imports DigitalData.Modules.Filesystem
Imports DigitalData.Modules.Logging
Imports DigitalData.Modules.Filesystem.FileContainer
Imports DigitalData.Modules.Filesystem.FileContainerOld
<TestClass()> Public Class FileContainerTest
@ -43,18 +43,18 @@ Imports DigitalData.Modules.Filesystem.FileContainer
<TestMethod()>
<ExpectedException(GetType(ArgumentNullException))>
Public Sub TestConstructorPasswordNothing()
Dim oContainer = New FileContainer(_logConfig, Nothing)
Dim oContainer = New FileContainerOld(_logConfig, Nothing)
End Sub
<TestMethod()>
Public Sub TestConstructorValidPassword()
Dim oContainer = New FileContainer(_logConfig, "foobar")
Dim oContainer = New FileContainerOld(_logConfig, "foobar")
End Sub
<TestMethod()>
<ExpectedException(GetType(FileNotFoundException))>
Public Sub TestAddFileNonExistentFilePath()
Dim oContainer As New FileContainer(_logConfig, PASSWORD)
Dim oContainer As New FileContainerOld(_logConfig, PASSWORD)
oContainer.AddFile(FILENAME_NONEXISTENT)
End Sub
@ -62,21 +62,21 @@ Imports DigitalData.Modules.Filesystem.FileContainer
<TestMethod()>
<ExpectedException(GetType(ArgumentNullException))>
Public Sub TestAddFileNothingFilePath()
Dim oContainer As New FileContainer(_logConfig, PASSWORD)
Dim oContainer As New FileContainerOld(_logConfig, PASSWORD)
oContainer.AddFile(Nothing)
End Sub
<TestMethod()>
Public Sub TestAddFileValidFilePath()
Dim oContainer As New FileContainer(_logConfig, PASSWORD)
Dim oContainer As New FileContainerOld(_logConfig, PASSWORD)
oContainer.AddFile(FILENAME_VALID)
End Sub
<TestMethod()>
Public Sub TestFilesPropertyCount()
Dim oContainer As New FileContainer(_logConfig, PASSWORD)
Dim oContainer As New FileContainerOld(_logConfig, PASSWORD)
oContainer.AddFile(FILENAME_VALID)
Assert.AreEqual(1, oContainer.Files.Count)
@ -84,7 +84,7 @@ Imports DigitalData.Modules.Filesystem.FileContainer
<TestMethod()>
Public Sub TestFilesPropertyType()
Dim oContainer As New FileContainer(_logConfig, PASSWORD)
Dim oContainer As New FileContainerOld(_logConfig, PASSWORD)
oContainer.AddFile(FILENAME_VALID)
Assert.IsInstanceOfType(oContainer.Files.First(), GetType(FileEntry))
@ -93,7 +93,7 @@ Imports DigitalData.Modules.Filesystem.FileContainer
<TestMethod()>
Public Sub TestSaveValidPath()
Dim oContainer As New FileContainer(_logConfig, PASSWORD)
Dim oContainer As New FileContainerOld(_logConfig, PASSWORD)
oContainer.AddFile(FILENAME_VALID)
oContainer.Save(CONTAINER_FILE_VALID)
@ -104,7 +104,7 @@ Imports DigitalData.Modules.Filesystem.FileContainer
<TestMethod()>
<ExpectedException(GetType(UnauthorizedAccessException))>
Public Sub TestSavePathNotWritable()
Dim oContainer As New FileContainer(_logConfig, PASSWORD)
Dim oContainer As New FileContainerOld(_logConfig, PASSWORD)
oContainer.AddFile(FILENAME_VALID)
oContainer.Save(CONTAINER_FILE_NOWRITE)
@ -112,13 +112,13 @@ Imports DigitalData.Modules.Filesystem.FileContainer
<TestMethod()>
Public Sub TestSaveLoadSameContents()
Dim oContainer As FileContainer
Dim oContainer As FileContainerOld
Dim oFileContents As String = "dasisteintest"
' Test String in Textdatei schreiben
IO.File.WriteAllText(FILENAME_VALID, oFileContents)
oContainer = New FileContainer(_logConfig, PASSWORD)
oContainer = New FileContainerOld(_logConfig, PASSWORD)
' Textdatei zu einem Container hinzufügen
oContainer.AddFile(FILENAME_VALID)
@ -131,7 +131,7 @@ Imports DigitalData.Modules.Filesystem.FileContainer
IO.File.Delete(FILENAME_VALID)
oContainer = Nothing
oContainer = New FileContainer(_logConfig, PASSWORD)
oContainer = New FileContainerOld(_logConfig, PASSWORD)
' Container wieder laden
oContainer.Load(CONTAINER_FILE_VALID)

View File

@ -8,12 +8,19 @@ Imports DigitalData.Modules.Logging
Friend Class Encryption
' This constant is used to determine the keysize of the encryption algorithm in bits.
' We divide this by 8 within the code below to get the equivalent number of bytes.
Private Const KEYSIZE As Integer = 256
Private Const KEY_SIZE As Integer = 256
' This constant determines the number of iterations for the password bytes generation function.
Private Const DERIVATION_ITERATIONS As Integer = 1000
Private Const BLOCK_SIZE As Integer = 256
Private _paddingMode As PaddingMode = PaddingMode.Zeros
Private _cipherMode As CipherMode = CipherMode.CBC
Private ReadOnly _password As String
Private _logger As Logger
Public Sub New(LogConfig As LogConfig, Password As String)
_logger = LogConfig.GetLogger()
@ -37,11 +44,12 @@ Friend Class Encryption
Dim oSaltStringBytes = Generate256BitsOfRandomEntropy()
Dim oIvStringBytes = Generate256BitsOfRandomEntropy()
Using oPassword = New Rfc2898DeriveBytes(_password, oSaltStringBytes, DERIVATION_ITERATIONS)
Dim oKeyBytes = oPassword.GetBytes(KEYSIZE / 8)
Dim oKeyBytes = oPassword.GetBytes(KEY_SIZE / 8)
Using oSymmetricKey = New RijndaelManaged()
oSymmetricKey.BlockSize = 256
oSymmetricKey.Mode = CipherMode.CBC
oSymmetricKey.Padding = PaddingMode.PKCS7
oSymmetricKey.BlockSize = BLOCK_SIZE
oSymmetricKey.Mode = _cipherMode
oSymmetricKey.Padding = _paddingMode
Using oEncryptor = oSymmetricKey.CreateEncryptor(oKeyBytes, oIvStringBytes)
Using oMemoryStream = New MemoryStream()
Using oCryptoStream = New CryptoStream(oMemoryStream, oEncryptor, CryptoStreamMode.Write)
@ -76,18 +84,18 @@ Friend Class Encryption
' Get the complete stream of bytes that represent:
' [32 bytes of Salt] + [32 bytes of IV] + [n bytes of CipherText]
' Get the saltbytes by extracting the first 32 bytes from the supplied cipherText bytes.
Dim oSaltStringBytes = cipherTextBytesWithSaltAndIv.Take(KEYSIZE / 8).ToArray()
Dim oSaltStringBytes = cipherTextBytesWithSaltAndIv.Take(KEY_SIZE / 8).ToArray()
' Get the IV bytes by extracting the next 32 bytes from the supplied cipherText bytes.
Dim oIvStringBytes = cipherTextBytesWithSaltAndIv.Skip(KEYSIZE / 8).Take(KEYSIZE / 8).ToArray()
Dim oIvStringBytes = cipherTextBytesWithSaltAndIv.Skip(KEY_SIZE / 8).Take(KEY_SIZE / 8).ToArray()
' Get the actual cipher text bytes by removing the first 64 bytes from the cipherText string.
Dim oCipherTextBytes = cipherTextBytesWithSaltAndIv.Skip((KEYSIZE / 8) * 2).Take(cipherTextBytesWithSaltAndIv.Length - ((KEYSIZE / 8) * 2)).ToArray()
Dim oCipherTextBytes = cipherTextBytesWithSaltAndIv.Skip((KEY_SIZE / 8) * 2).Take(cipherTextBytesWithSaltAndIv.Length - ((KEY_SIZE / 8) * 2)).ToArray()
Using oPassword = New Rfc2898DeriveBytes(_password, oSaltStringBytes, DERIVATION_ITERATIONS)
Dim oKeyBytes = oPassword.GetBytes(KEYSIZE / 8)
Dim oKeyBytes = oPassword.GetBytes(KEY_SIZE / 8)
Using oSymmetricKey = New RijndaelManaged()
oSymmetricKey.BlockSize = 256
oSymmetricKey.Mode = CipherMode.CBC
oSymmetricKey.Padding = PaddingMode.PKCS7
oSymmetricKey.BlockSize = BLOCK_SIZE
oSymmetricKey.Mode = _cipherMode
oSymmetricKey.Padding = _paddingMode
Using oDecryptor = oSymmetricKey.CreateDecryptor(oKeyBytes, oIvStringBytes)
Using oMemoryStream = New MemoryStream(oCipherTextBytes)
Using oCryptoStream = New CryptoStream(oMemoryStream, oDecryptor, CryptoStreamMode.Read)

View File

@ -1,10 +1,10 @@
Imports System.IO
Imports System.Runtime.Serialization.Formatters.Binary
Imports DigitalData.Modules.Logging
Imports ProtoBuf
''' <module>FileContainer</module>
''' <version>0.0.0.1</version>
''' <date>16.11.2018</date>
''' <version>0.0.0.2</version>
''' <date>21.11.2018</date>
''' <summary>
''' File Container for securely saving files
''' </summary>
@ -14,276 +14,190 @@ Imports DigitalData.Modules.Logging
''' <params>
''' LogConfig, DigitalData.Module.Logging.LogConfig
''' A LogConfig object
''' Password, String
''' The Password to Encrypt
''' Path, String
''' The Path to save/load the container
''' </params>
''' <props>
''' </props>
''' <example>
'''
'''
'''
'''
''' dim oContainer = Container.Create(logConfig, "pass", "E:\some.container")
''' dim oContainer = Container.Load(logConfig, "pass", "E:\some.container")
'''
''' dim oContainer = new Container(logConfig, "pass", "E:\some.container")
''' oContainer.Save()
'''
''' dim oContainer = new Container(logConfig, "pass", "E:\some.container")
''' oContainer.Contents = oSomeData
''' oContainer.Save()
'''
''' dim oContainer = new Container(logConfig, "pass", "E:\some.container")
''' oContainer.Load()
''' dim oContents = oContainer.Contents
'''
''' dim oContainer = new Container(logConfig, "pass", "E:\some.container")
''' oContainer.Load()
''' oContainer.Contents = oSomeOtherData
''' oContainer.Save()
''' oContainer.SaveAs("E:\some2.container")
''' </example>
''' <remarks>
''' </remarks>
Public Class FileContainer
Private _files As List(Of FileEntry)
Private _crypto As Encryption
Private _compression As Compression
Private _formatter As BinaryFormatter
Private _containerId As Guid
Private _inner As FileContainerInner
Private _logger As Logger
Private _logConfig As LogConfig
Private _path As String
<Serializable>
Public Class FileEntry
Public FileId As String
Public Contents As Byte()
Public CreatedAt As DateTime
Public UpdatedAt As DateTime
End Class
<Serializable>
Public Class FileContainerInner
Public Files As List(Of FileEntry)
Public ContainerID As Guid
Public CreatedAt As DateTime
Public UpdatedAt As DateTime
End Class
Public ReadOnly Property ContainerId As String
Public Property Contents As Byte()
Get
If _inner Is Nothing Then
Return Nothing
End If
Return _inner.ContainerID.ToString
Return _inner.Contents
End Get
Set(value As Byte())
_inner.Contents = value
End Set
End Property
Public Property Extension As String
Get
Return _inner.Extension
End Get
Set(value As String)
_inner.Extension = value
End Set
End Property
Public ReadOnly Property FileId As String
Get
Return _inner.FileId
End Get
End Property
Public ReadOnly Property CreatedAt As String
Get
Return _inner.CreatedAt
End Get
End Property
Public ReadOnly Property UpdatedAt As String
Get
Return _inner.UpdatedAt
End Get
End Property
''' <summary>
''' Gibt eine Auflistung der Dateien in Container zurück.
''' </summary>
''' <returns>Eine Liste von Objekten der Klasse FileContainer.FileEntry</returns>
Public ReadOnly Property Files As List(Of FileEntry)
Get
If _inner Is Nothing Then
Return Nothing
End If
Public Shared Function Create(LogConfig As LogConfig, Password As String) As FileContainer
Dim oContainer = New FileContainer(LogConfig, Password)
Return oContainer
End Function
Return _inner.Files
End Get
End Property
Public Shared Function Load(LogConfig As LogConfig, Password As String, Path As String) As FileContainer
Dim oContainer = New FileContainer(LogConfig, Password, Path)
oContainer.Load()
Return oContainer
End Function
Public Sub New(LogConfig As LogConfig, Password As String, Path As String)
_logger = LogConfig.GetLogger()
_crypto = New Encryption(LogConfig, Password)
_compression = New Compression(LogConfig)
_inner = New FileContainerInner()
_path = Path
End Sub
''' <summary>
''' Erstellt eine Representation eines Datei Containers, der mit dem angegebenen Passwort geschützt ist.
''' Ist für das speichern von neuen Containern und für das laden vorhandener Container nötig.
''' </summary>
''' <param name="Password">Das Passwort, mit dem der Container ver- und entschlüsselt wird</param>
''' <example>
''' Dim password = "meinpasswort"
''' Dim container = new FileContainer(password)
''' </example>
Public Sub New(LogConfig As LogConfig, Password As String)
_logger = LogConfig.GetLogger()
_crypto = New Encryption(LogConfig, Password)
_compression = New Compression(LogConfig)
_formatter = New BinaryFormatter
_inner = New FileContainerInner() With {
.Files = New List(Of FileEntry),
.ContainerID = Guid.NewGuid(),
.CreatedAt = New DateTime()
}
_inner = New FileContainerInner()
End Sub
''' <summary>
''' Speichert einen Datei Container am angegebenen Pfad.
''' Davor werden die Dateien in Files komprimiert und dann verschlüsselt.
''' </summary>
''' <param name="Path">Der Pfad mit Dateiname, unter dem der Container abgelegt wird.</param>
''' <see cref="Files"/>
''' <example>
''' container.AddFile("datei1.txt")
''' container.AddFile("datei2.pdf")
''' container.Save("container.enc")
''' </example>
Public Sub Save(Path As String)
Try
' 1. Serialize
Dim oFileContainerInner As Byte() = Serialize(_inner)
' 2. Compress
Dim oFileContainerCompressed As Byte() = _compression.Compress(oFileContainerInner)
' 3. Encrypt
Dim oFileContainerEncrypted As Byte() = _crypto.Encrypt(oFileContainerCompressed)
BytesToFile(oFileContainerEncrypted, Path)
Catch ex As Exception
_logger.Error(ex)
Throw ex
End Try
Public Sub SetFile(Contents As Byte(), Extension As String)
_inner.Contents = Contents
_inner.Extension = Extension
End Sub
''' <summary>
''' Speichert einen Datei Container am angegebenen Pfad.
''' Davor werden die Dateien in Files komprimiert und dann verschlüsselt.
''' </summary>
''' <param name="Path">Der Pfad mit Dateiname, unter dem der Container abgelegt wird.</param>
''' <example>
''' Public Async Sub foobar()
''' ...
''' container.AddFile("datei1.txt")
''' container.AddFile("datei2.pdf")
''' await container.SaveAsync("container.enc")
''' ...
''' End Sub
''' </example>
Public Async Function SaveAsync(Path As String) As Task
Try
' 1. Serialize
Dim oFileContainerInner As Byte() = Serialize(_inner)
' 2. Compress
Dim oFileContainerCompressed As Byte() = Await _compression.CompressAsync(oFileContainerInner)
'Dim bFilesCompressed = bFiles
' 3. Encrypt
Dim oFileContainerEncrypted As Byte() = Await _crypto.EncryptAsync(oFileContainerCompressed)
BytesToFileAsync(oFileContainerEncrypted, Path)
Catch ex As Exception
_logger.Error(ex)
Throw ex
End Try
End Function
''' <summary>
''' Lädt einen Datei Container aus dem angegebenen Pfad.
''' Anschließend werden die enthaltenen Dateien entschlüsselt, dekomprimiert und in Files abgelegt.
''' </summary>
''' <param name="Path">Der Pfad mit Dateiname, von dem der Container geladen wird.</param>
''' <see cref="Files"/>
''' <example>
''' container.Load("container.enc")
''' Dim files As List(Of FileContainer.FileEntry) = container.Files
''' </example>
Public Sub Load(Path As String)
Try
Dim oContainerInnerEncrypted As Byte() = FileToBytes(Path)
' 1. Decrypt
Dim oFileContainerCompressed As Byte() = _crypto.Decrypt(oContainerInnerEncrypted)
' 2. Decompress
Dim oFileContainerInner As Byte() = _compression.Decompress(oFileContainerCompressed)
' 3. Deserialize
_inner = Deserialize(oFileContainerInner)
Catch ex As Exception
_logger.Error(ex)
Throw ex
End Try
End Sub
''' <summary>
''' Lädt einen Datei Container aus dem angegebenen Pfad.
''' Anschließend werden die enthaltenen Dateien entschlüsselt, dekomprimiert und in Files abgelegt.
''' </summary>
''' <param name="Path">Der Pfad mit Dateiname, von dem der Container geladen wird.</param>
''' <example>
''' Public Async Sub foobar()
''' ...
''' await container.LoadAsync("container.enc")
''' Dim files As List(Of FileContainer.FileEntry) = container.Files
''' ...
''' End Sub
''' </example>
Public Async Function LoadAsync(Path As String) As Task
Try
Dim oContainerInnerEncrypted As Byte() = Await FileToBytesAsync(Path)
' 1. Decrypt
Dim oFileContainerCompressed As Byte() = Await _crypto.DecryptAsync(oContainerInnerEncrypted)
' 2. Decompress
Dim oFileContainerInner As Byte() = Await _compression.DecompressAsync(oFileContainerCompressed)
' 3. Deserialize
_inner = Deserialize(oFileContainerInner)
Catch ex As Exception
_logger.Error(ex)
Throw ex
End Try
End Function
''' <summary>
''' Fügt einem Datei Container eine neue Datei hinzu
''' </summary>
''' <param name="FilePath">Der Pfad zur Datei, die im Container gespeichert werden soll</param>
''' <exception cref="FileNotFoundException" />
Public Function AddFile(FilePath As String) As String
Try
Dim oFileInfo As New FileInfo(FilePath)
If oFileInfo.Exists = False Then
Throw New FileNotFoundException($"{FilePath} is not a valid path.")
Public Sub Save()
If IsNothing(_path) Then
Throw New ArgumentException("Path not set")
End If
Return AddFileAsByteArray(IO.File.ReadAllBytes(FilePath))
Catch ex As Exception
_logger.Error(ex)
Throw ex
End Try
End Function
SaveAs(_path)
End Sub
Public Function AddFileAsByteArray(FileContents As Byte()) As String
Public Sub SaveAs(Path As String)
Try
Dim oFileEntry As New FileEntry With {
.Contents = FileContents,
.FileId = Guid.NewGuid().ToString()
}
_inner.Files.Add(oFileEntry)
Return oFileEntry.FileId
WriteBytesToFile(TransformToBytes(_inner), Path)
Catch ex As Exception
_logger.Error(ex)
Throw ex
End Try
End Sub
Public Sub Load()
If IsNothing(_path) Then
Throw New ArgumentException("Path not set")
End If
LoadFrom(_path)
End Sub
Public Sub LoadFrom(Path As String)
Try
_inner = TransformToObject(ReadBytesFromFile(_path))
Catch ex As Exception
_logger.Error(ex)
Throw ex
End Try
End Sub
Private Function TransformToBytes([Object] As FileContainerInner) As Byte()
Dim oBytes = Serialize([Object])
Dim oCompressed = _compression.Compress(oBytes)
Dim oEncrypted = _crypto.Encrypt(oCompressed)
Return oEncrypted
End Function
Public Function GetFile(FileId As String) As FileEntry
Return _inner.Files.Where(Function(f) f.FileId = FileId).FirstOrDefault()
Private Function TransformToObject(Bytes As Byte()) As FileContainerInner
Dim oDecrypted = _crypto.Decrypt(Bytes)
Dim oDecompressed = _compression.Decompress(oDecrypted)
Dim oObject = Deserialize(oDecompressed)
Return oObject
End Function
Private Function Serialize(InnerData As FileContainerInner) As Byte()
Dim oBinaryData As Byte()
Using oStream As New MemoryStream
_formatter.Serialize(oStream, InnerData)
Return oStream.ToArray()
Serializer.Serialize(oStream, InnerData)
oBinaryData = oStream.ToArray()
End Using
Return oBinaryData
End Function
Private Function Deserialize(InnerData As Byte()) As FileContainerInner
Dim oObject As FileContainerInner
Using oStream As New MemoryStream(InnerData)
Dim oInner = TryCast(_formatter.Deserialize(oStream), FileContainerInner)
Return oInner
oObject = Serializer.Deserialize(Of FileContainerInner)(oStream)
End Using
Return oObject
End Function
Private Sub BytesToFile(Data As Byte(), FilePath As String)
Using oSourceStream As New FileStream(FilePath, FileMode.Append, FileAccess.Write, FileShare.None)
Private Sub WriteBytesToFile(Data As Byte(), FilePath As String)
Using oSourceStream As New FileStream(FilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None)
oSourceStream.Write(Data, 0, Data.Length)
oSourceStream.Flush()
End Using
End Sub
Private Function FileToBytes(FilePath As String) As Byte()
Return IO.File.ReadAllBytes(FilePath)
End Function
Private Async Function FileToBytesAsync(FilePath As String) As Task(Of Byte())
Using oFileStream = New FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, True)
Private Function ReadBytesFromFile(FilePath As String) As Byte()
Using oFileStream = New FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096)
Dim oBuffer As Byte() = New Byte(oFileStream.Length - 1) {}
Await oFileStream.ReadAsync(oBuffer, 0, oFileStream.Length)
oFileStream.Read(oBuffer, 0, oFileStream.Length)
oFileStream.Close()
Return oBuffer
End Using
End Function
Private Async Sub BytesToFileAsync(Data As Byte(), FilePath As String)
Using oSourceStream As New FileStream(FilePath, FileMode.Append, FileAccess.Write, FileShare.None, 4096, True)
Await oSourceStream.WriteAsync(Data, 0, Data.Length)
Await oSourceStream.FlushAsync()
End Using
End Sub
End Class

View File

@ -0,0 +1,22 @@
Imports ProtoBuf
<Serializable>
<ProtoContract>
Public Class FileContainerInner
<ProtoMember(1)>
Public FileId As String
<ProtoMember(2)>
Public Contents As Byte()
<ProtoMember(3)>
Public CreatedAt As DateTime
<ProtoMember(4)>
Public UpdatedAt As DateTime
<ProtoMember(5)>
Public Extension As String
Public Sub New()
FileId = Guid.NewGuid.ToString
CreatedAt = Date.Now
End Sub
End Class

View File

@ -47,6 +47,9 @@
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.5.10\lib\net45\NLog.dll</HintPath>
</Reference>
<Reference Include="protobuf-net, Version=2.4.0.0, Culture=neutral, PublicKeyToken=257b51d87d2e4d67, processorArchitecture=MSIL">
<HintPath>..\packages\protobuf-net.2.4.0\lib\net40\protobuf-net.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Data" />
@ -73,9 +76,10 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Compression.vb" />
<Compile Include="FileContainer.vb" />
<Compile Include="Encryption.vb" />
<Compile Include="File.vb" />
<Compile Include="FileContainer.vb" />
<Compile Include="FileContainerInner.vb" />
<Compile Include="My Project\AssemblyInfo.vb" />
<Compile Include="My Project\Application.Designer.vb">
<AutoGen>True</AutoGen>

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="NLog" version="4.5.10" targetFramework="net461" />
<package id="protobuf-net" version="2.4.0" targetFramework="net461" />
</packages>