David Wilson David Wilson - 2 months ago 20
Vb.net Question

Serializing encrypted Class properties

I'm writing a class that stores email account information. Some of the properties like email address, account password and the server names are stored in encrypted form in Private variables.

The Property Setter encrypts the data and the Getter decrypts it.

Yes I know that storing an encryption key within the software is inherently insecure, but that code will be changed later when I can figure out an alternative. Probably using a key entered by the user or a password that will be used to generate the correct key. At the moment though, the program is for personal use.

When I try to serialize the encrypted information, the information is sent to the xml file in plain text because the information is accessed using the Property Getter which decrypts the data of course.

I don't particularly want to have to expose an extra encrypted property and mark the unencrypted one to be ignored by the serializer - it seems inelegant and possibly less secure.

I could of course do the de/encryption in the main program before it is passed to the property setter, but I wonder if this could be done within the class to keep things neater if I want to reuse the 'EmailAccount' class.

Also I would prefer for the time being at least to have the xml file in plain text so that I can make sure that the en/decryption is working. I realize that I can encrypt the entire file, but that is for later.

Below is a reduced version of the 'EmailAccount' class ..

Public Class EmailAccount

Public Enum UseSSL
Yes
No
End Enum

Dim _imapPwd As String
Dim _smtpPwd As String
Dim _user As String
Dim _imapServer As String
Dim _smtpServer As String
Dim _usessl As UseSSL
Dim _imapPort As Integer
Dim _smtpPort As Integer

Public Property User As String
Set(value As String)
_user = Encrypt(value)
End Set
Get
Return Decrypt(_user)
End Get
End Property

Public Property ImapPassword As String
Set(value As String)
_imapPwd = Encrypt(value)
End Set
Get
Return Decrypt(_imapPwd)
End Get
End Property

Public Property ImapServer As String
Set(value As String)
_imapServer = Encrypt(value)
End Set
Get
Return Decrypt(_imapServer)
End Get
End Property


Public Sub New()
User = "noemailset@nowhere.com"
ImapPassword = "nopasswordfddfsg"
ImapServer = "no.imap.server"
SmtpPassword = "nopasswordsdfgdf"
SmtpServer = "no.smtp.server"
ImapPort = -1
SmtpPort = -1
SslState = UseSSL.Yes
End Sub

Private Function Encrypt(clearText As String) As String
Dim EncryptionKey As String = "crypto key goes here"
Dim clearBytes As Byte() = Encoding.Unicode.GetBytes(clearText)
Using encryptor As Aes = Aes.Create()
Dim pdb As New Rfc2898DeriveBytes(EncryptionKey, New Byte() {&H49, &H76, &H61, &H6E, &H20, &H4D,
&H65, &H64, &H76, &H65, &H64, &H65,
&H76})
encryptor.Key = pdb.GetBytes(32)
encryptor.IV = pdb.GetBytes(16)
Using ms As New MemoryStream()
Using cs As New CryptoStream(ms, encryptor.CreateEncryptor(), CryptoStreamMode.Write)
cs.Write(clearBytes, 0, clearBytes.Length)
cs.Close()
End Using
clearText = Convert.ToBase64String(ms.ToArray())
End Using
End Using
Return clearText
End Function

Private Function Decrypt(cipherText As String) As String
Dim EncryptionKey As String = "crypto key goes here"
Dim cipherBytes As Byte() = Convert.FromBase64String(cipherText)
Using encryptor As Aes = Aes.Create()
Dim pdb As New Rfc2898DeriveBytes(EncryptionKey, New Byte() {&H49, &H76, &H61, &H6E, &H20, &H4D,
&H65, &H64, &H76, &H65, &H64, &H65,
&H76})
encryptor.Key = pdb.GetBytes(32)
encryptor.IV = pdb.GetBytes(16)
Using ms As New MemoryStream()
Using cs As New CryptoStream(ms, encryptor.CreateDecryptor(), CryptoStreamMode.Write)
cs.Write(cipherBytes, 0, cipherBytes.Length)
cs.Close()
End Using
cipherText = Encoding.Unicode.GetString(ms.ToArray())
End Using
End Using
Return cipherText
End Function


End Class


In my main form I have the procedure below to serialize the data.

''' <summary>
''' Convert a class state into XML
''' </summary>
''' <typeparam name="T">The type of object</typeparam>
''' <param name="obj">The object to serilize</param>
''' <param name="FilePath">The path to the XML</param>
Public Shared Sub Serialize(Of T)(ByVal obj As T, Filepath As String)
Dim XmlBuddy As New System.Xml.Serialization.XmlSerializer(GetType(T))
Dim MySettings As New System.Xml.XmlWriterSettings()
MySettings.Indent = True
MySettings.CloseOutput = True
Dim MyWriter As System.Xml.XmlWriter = System.Xml.XmlWriter.Create(Filepath, MySettings)
XmlBuddy.Serialize(MyWriter, obj)
MyWriter.Flush()
MyWriter.Close()
End Sub

Answer

You could do the same thing you are doing in your crypto and use a CryptoStream with the serializer stream. This uses the BinaryFormatter:

Private Sub SaveEmails(data As List(Of EmailAccount), password As String,
                   Filepath As String)
    ' ToDo: further hashing, maybe
    Dim key As Byte() = GetPasswordHash(passW, 32, 62700)
    Dim iv(15) As Byte
    Using rng As New RNGCryptoServiceProvider
        rng.GetNonZeroBytes(iv)
    End Using

    ' ToDo: Try/Catch
    If File.Exists(Filepath) Then File.Delete(Filepath)

    Using rij = Rijndael.Create()
        rij.Padding = PaddingMode.ISO10126

        Using cryptor As ICryptoTransform = rij.CreateEncryptor(key, iv),
            fs As New FileStream(Filepath, FileMode.CreateNew, FileAccess.Write)
            ' write the iv to bare stream
            fs.Write(iv, 0, iv.Length)

            Using cs = New CryptoStream(fs, cryptor, CryptoStreamMode.Write)
                Dim bf = New BinaryFormatter
                bf.Serialize(cs, data)
                ' may not be needed - doesnt hurt
                cs.FlushFinalBlock()
            End Using
        End Using
    End Using
End Sub

Private Function LoadEmails(passW As String, 
                        Filepath As String) As List(Of EmailAccount)
    Dim iv(15) As Byte
    Dim key As Byte() = GetPasswordHash(passW, 32, 62700)

    Dim emails As List(Of EmailAccount)

    Using rijAlg = Rijndael.Create()
        rijAlg.Padding = PaddingMode.ISO10126

        ' to do: check if exists
        Using fs As New FileStream(Filepath, FileMode.Open)
            ' read the IV first
            fs.Read(iv, 0, iv.Length)
            ' USING encryptor AND CryptoStream
            Using encryptor As ICryptoTransform = rijAlg.CreateDecryptor(key, iv),
                cs As New CryptoStream(fs, encryptor, CryptoStreamMode.Read)
                Dim bf As New BinaryFormatter
                emails = CType(bf.Deserialize(cs), List(Of EmailAccount))
            End Using
        End Using
    End Using

    Return emails
End Function

Private Function GetPasswordHash(pw As String, count As Int32, iter As Int32) As Byte()
    Dim salt As Byte() = {&H49, &H76, &H61, &H6E, &H20, &H4D,
                          &H65, &H64, &H76, &H65, &H64, &H65, &H76}

    Using pbkdf As New Rfc2898DeriveBytes(pw, salt,
                                          iter)
        Return pbkdf.GetBytes(count)
    End Using
End Function

One difference is that a random IV is used and it is written to and read from the base stream so that a new one can be used each time you save the file and you do not have to save it anywhere. These could be shared methods of a Crypto class to create the CryptoStream so that code can be worked on separately.

The password is also hashed a little differently. That hasher is separate so if you change the iterations or method, the encryptor and decryptor dont end up using a different method.

Usage, Testing:

Dim EmailAccts As New List(Of EmailAccount)
EmailAccts.Add(New EmailAccount With {.User = "Ziggy@ziggysplace.net", .SMTPPass = "secret!pass",
                                      .UseSSL = True, .SMTPServer = "yadayada", .SMTPPort = -42})
EmailAccts.Add(New EmailAccount With {.User = "david@somewhere.com", .SMTPPass = "$tack!overflow$",
                                      .UseSSL = False, .SMTPServer = "blahblah", .SMTPPort = 314})

Dim password As String = "AWeak!pa$$WorD!"

SaveEmails(EmailAccts, password, "C:\Temp\secretemails.bin")
Dim newEmails = LoadEmails(password, "C:\Temp\secretemails.bin")

' is everyone ok?
For Each email In newEmails
    Console.WriteLine("{0}  ... {1}", email.User, email.SMTPPass)
Next

Mine was just a simple class of auto properties since that crypto isnt being used. Everyone survived:

Ziggy@ziggysplace.net ... secret!pass
david@somewhere.com ... $tack!overflow$

Comments