Attribute VB_Name = "basMySecret"
Option Explicit
Option Base 0

' $Id: basMySecret $

'****************************************************************************
' Copyright (C)2007 DI Management Services Pty Limited, All Rights Reserved.
'****************************************************************************
' Distribution: You can freely use this code in your own applications, but
' you may not reproduce or publish this code on any web site, online service,
' or distribute as source on any media without express permission.
' Terms: Use at your own risk. Provided "as is" with no warranties.
' Use of CryptoSys API requires a licence for non-personal use.
' Contact: <www.di-mgt.com.au> <www.cryptosys.net>
'****************************************************************************
' This file last updated:
'   $Date: 2007-05-20 20:35:00 $
'   $Author: dai $
'****************************************************************************

' This code shows how you can use CryptoSys API to create and read MySecret
' data. This code is provided merely to DEMONSTRATE the principles involved.
' It is not meant as an example of production code.
' At the very minimum:
'   * Remove all the debugging statments
'   * Add proper error handling

' MySecret Reference: <http://www.di-mgt.com.au/mysecret.html#technical>

' This code requires the CryptoSys API library to be installed on your system
' and the `basCryptoSys.bas' module to be included in your project.
' Download CryptoSys API from <http://www.cryptosys.net/#api>
' Commercial use requires a licence: <http://www.cryptosys.net/purchase.html>

' *************************
' INTERNAL CONSTANTS WE USE
' *************************
Private Const BLOCK_LEN As Long = 8
Private Const FRAME_LEN As Long = 60
Private Const STRETCH_COUNT As Long = 1024
Private Const FRAME_BEGIN As String = "-----BEGIN MYSECRET-----"
Private Const FRAME_END As String = "-----END MYSECRET-----"
Private Const FRAME_STUB As String = "-----BEGIN MYSECRET"

' ******
' TESTS
' ******
' To test a freshly-encrypted-and-so-different output, try this:
'   ? MySecret_Decrypt(Test_MySecret_Encrypt(), "password")

Public Function Test_MySecret_Encrypt() As String
    Dim strPlain As String
    Dim strPassword As String
    Dim strOutput As String

    ' INPUT PASSWORD
    strPassword = "password"
    ' INPUT PLAINTEXT
    strPlain = "KING RICHARD. A horse! a horse! my kingdom for a horse!" & vbCrLf & _
       "CATESBY. Withdraw, my lord! I'll help you to a horse." & vbCrLf & _
       "KING RICHARD. Slave, I have set my life upon a cast" & vbCrLf & _
       "And I Will stand the hazard of the die." & vbCrLf & _
       "I think there be six Richmonds in the field;" & vbCrLf & _
       "Five have I slain to-day instead of him." & vbCrLf & _
       "A horse! a horse! my kingdom for a horse!             Exeunt"
       
    ' Do the business
    strOutput = MySecret_Encrypt(strPlain, strPassword)
    Debug.Print strOutput
    
    ' Clear the password
    Call WIPE_String(strPassword, Len(strPassword))
    
    Test_MySecret_Encrypt = strOutput

End Function

Public Function Test_MySecret_Decrypt() As String
    Dim strInput As String
    Dim strPassword As String
    Dim strOutput As String
    
    ' INPUT PASSWORD
    strPassword = "password"
    ' INPUT MYSECRET-DATA
    strInput = "-----BEGIN MYSECRET-----" & vbCrLf & _
        "TVn8AEFK9uRT91snxkz5y2TguL39hcNxEjRctQzipRaagPLTe0tYQikxIPlQ" & vbCrLf & _
        "UPHGKDJVK3c9UTcNOqCa2eDmdJaUIJoAXITLeCdLMRfe+I0QYuBVDAqHpQwz" & vbCrLf & _
        "2LscsDWlNHiqbGYeP0IyYiv0tk7mzKwj9PSSiKK5xtdiXo29BJR2+JtiOQTc" & vbCrLf & _
        "QgZzURnAWIpc5/INeSJ/6rm4BZRU2Db/sMgdMLrNSIBeKEP+FKJ58/wAUems" & vbCrLf & _
        "2q03DMsIcs2Scp78JabqF6RbzYaHTDH2tV8fmAGVXnCTXKaor7dk1ZuWD0Ny" & vbCrLf & _
        "D0azfzNqGZm/hgp15HGDrXqkJdEq/XlMulOnxKyzqx0pJkYqNwxDHCDFE8MN" & vbCrLf & _
        "KOqyeUhqm+LOjq6I6iUhaLoMVAtIew==" & vbCrLf & _
        "-----END MYSECRET-----"
        
    ' Do the business
    strOutput = MySecret_Decrypt(strInput, strPassword)
    Debug.Print strOutput
    
    ' Clear the password
    Call WIPE_String(strPassword, Len(strPassword))
    
    Test_MySecret_Decrypt = strOutput
    
End Function

' ***********************
' MAIN EXPORTED FUNCTIONS
' ***********************
Public Function MySecret_Encrypt(strPlain As String, strPassword As String) As String
' INPUT:  plaintext string, password
' OUTPUT: string of MySecret v3 base64-encoded ciphertext (with framing and CR-LFs)

    Dim strOutput As String
    Dim abPlain() As Byte
    Dim nPlain As Long
    Dim abCompr() As Byte
    Dim nCompr As Long
    Dim abBlock() As Byte
    Dim nBlock As Long
    Dim abPad() As Byte
    Dim nPad As Long
    Dim abKey() As Byte
    Dim nKey As Long
    Dim abEncode() As Byte
    Dim nEncode As Long
    Dim abInitVec() As Byte
    Dim abNumber() As Byte
    Dim strBase64 As String
    Dim nCrc24 As Long
    Dim k As Long
    Dim i As Long
    Dim iOffset As Long
    Dim iCount As Long
    Dim nChars As Long
    Dim nRet As Long
 
    If Len(strPlain) = 0 Then
        Debug.Print "ERROR: nothing to encrypt!"
        Exit Function
    End If
    
    Debug.Print "PLAINTEXT: [" & strPlain & "]"

    ' CONVERT input string to a byte array
    abPlain = StrConv(strPlain, vbFromUnicode)
    nPlain = UBound(abPlain) - LBound(abPlain) + 1
    Debug.Print "Plaintext length = " & nPlain & " bytes"
    
    ' COMPUTE THE CRC-24 CHECKSUM of the original plaintext
    nCrc24 = CRC24_Bytes(abPlain)
    Debug.Print "CRC-24 = " & Hex(nCrc24)
    
    ' COMPRESS using ZLIB_Deflate function
    ' -- first get the compressed length
    nCompr = ZLIB_Deflate(vbNull, 0, abPlain(0), nPlain)
    If nCompr <= 0 Then
        Debug.Print "ERROR: compression failed."
        GoTo Clean_Up
    End If
    Debug.Print "Compressed length = " & nCompr & " bytes"
    ' -- then do the compression
    ReDim abCompr(nCompr - 1)
    nCompr = ZLIB_Deflate(abCompr(0), nCompr, abPlain(0), nPlain)
    Debug.Print "Compressed data (hex)=" & cnvHexStrFromBytes(abCompr)
    
    ' CREATE A PADDING STRING OF LENGTH NPAD (actually a Byte array)
    '|<--------------NPAD--------------->|
    '|<---8--->|<----(8*k)---->|<-[1,8]->|
    '+---------+---------------+---------+
    '|  GUARD  |Random bytes...|  FINAL  |
    '+---------+---------------+---------+
    ' GUARD = 8 bytes of value NPAD
    ' FINAL = [1,8] bytes of value NPAD
    ' k = random number [0,7]
    ' Min(NPAD) = 8 + 8*0 + 1 = 9
    ' Max(NPAD) = 8 + 8*7 + 8 = 72
    
    ' Compute final number of odd bytes [1,8] to make length an exact multiple of 8
    nPad = BLOCK_LEN - (10 + nCompr + 3) Mod BLOCK_LEN
    ' -- pick a random number between 0 and 7
    k = RNG_Long(0, 7, "")
    Debug.Print "Random blocks = " & k
    ' -- increase size for extra blocks of 8
    nPad = nPad + (k + 1) * BLOCK_LEN
    Debug.Print "Padding string = " & nPad & " bytes = 0x" & Hex(nPad)
    ' Fill with random bytes
    ' (it's easier to fill the whole with random and then overwrite)
    ReDim abPad(nPad - 1)
    Call RNG_NonceData(abPad(0), nPad)
    ' Set value of bytes in guard and final blocks equal to NPAD
    For i = 0 To 7
        abPad(i) = CByte(nPad)
    Next
    For i = ((k + 1) * BLOCK_LEN) To nPad - 1
        abPad(i) = CByte(nPad)
    Next
    Debug.Print "Padding string (hex)=" & cnvHexStrFromBytes(abPad)
    
    ' COMPOSE the encryption block
    '|<------------------------nBlock--------------------------->|
    '|<--------10--------->|<------nCompr------>|<-3-->|<-nPad-->|
    '+-----+-------+-------+--------------------+------+---------+
    '|ZS(2)|CLEN(4)|ULEN(4)| Compressed data... |CRC(3)|PAD(9-72)|
    '+-----+-------+-------+--------------------+------+---------+
    nBlock = 10 + nCompr + 3 + nPad
    Debug.Print "Encryption block = " & nBlock & " bytes = " & nBlock / BLOCK_LEN & " blocks"
    ReDim abBlock(nBlock - 1)
    ' -- Two-byte signature 0x5A04
    abBlock(0) = &H5A   ' =Asc("Z")
    abBlock(1) = &H4    ' =0x04
    iOffset = 2
    ' -- convert CLEN and ULEN integers into 4-byte arrays in big-endian order
    ' -- and add to block
    abNumber = BytesFromLong(nCompr)
    For i = 0 To 3
        abBlock(iOffset) = abNumber(i)
        iOffset = iOffset + 1
    Next
    abNumber = BytesFromLong(nPlain)
    For i = 0 To 3
        abBlock(iOffset) = abNumber(i)
        iOffset = iOffset + 1
    Next
    ' -- add the compressed data
    For i = 0 To nCompr - 1
        abBlock(iOffset) = abCompr(i)
        iOffset = iOffset + 1
    Next
    ' -- convert the CRC-24 value into an array and add the last THREE bytes
    abNumber = BytesFromLong(nCrc24)
    For i = 1 To 3  ' NOTE: we only add three bytes here
        abBlock(iOffset) = abNumber(i)
        iOffset = iOffset + 1
    Next
    ' -- add the padding string
    For i = 0 To nPad - 1
        abBlock(iOffset) = abPad(i)
        iOffset = iOffset + 1
    Next
    Debug.Print "Encr block (before)=    " & cnvHexStrFromBytes(abBlock)
    
    ' GENERATE a random 8-byte initialization vector
    ReDim abInitVec(BLOCK_LEN - 1)
    Call RNG_NonceData(abInitVec(0), BLOCK_LEN)
    Debug.Print "IV (hex)=" & cnvHexStrFromBytes(abInitVec)
    
    ' CREATE the 128-bit key using stretching with the IV as a salt
    abKey = MakeStretchedKey(strPassword, abInitVec, BLOCK_LEN, STRETCH_COUNT)
    nKey = UBound(abKey) + 1
    Debug.Print "KEY (hex)=" & cnvHexStrFromBytes(abKey)
    
    ' ENCRYPT THE BLOCK using Blowfish in CBC mode using the key and IV
    nRet = BLF_BytesMode(abBlock(0), abBlock(0), nBlock, abKey(0), nKey, ENCRYPT, "CBC", abInitVec(0))
    Debug.Print "BLF_BytesMode returns " & nRet & " (expecting 0)"
    If nRet <> 0 Then
        Debug.Print "ERROR: encryption operation failed!"
        GoTo Clean_Up
    End If
    Debug.Print "Encr block (after) =    " & cnvHexStrFromBytes(abBlock)
    
    ' ADD THE MAIN HEADER to the ciphertext block
    ReDim abEncode(4 + BLOCK_LEN + nBlock - 1)
    ' -- 4-byte header
    abEncode(0) = &H4D  ' "M"
    abEncode(1) = &H59  ' "Y"
    abEncode(2) = &HFC
    abEncode(3) = &H0
    ' -- 8-byte IV
    iOffset = 4
    For i = 0 To BLOCK_LEN - 1
        abEncode(iOffset + i) = abInitVec(i)
    Next
    ' -- ciphertext block
    iOffset = 4 + BLOCK_LEN
    For i = 0 To nBlock - 1
        abEncode(iOffset + i) = abBlock(i)
    Next
    nEncode = 4 + BLOCK_LEN + nBlock
    Debug.Print "To encode (hex) ="
    Debug.Print "<-------HEADER---------><--ENCR BLOCK-->>"
    Debug.Print cnvHexStrFromBytes(abEncode)
    
    ' ENCODE TO BASE 64
    strBase64 = cnvB64StrFromBytes(abEncode)
    Call WIPE_Data(abEncode(0), nEncode)
    Debug.Print "Base64=" & strBase64
    
    ' ADD FRAMING AND LINE-BREAKS every 60 chars
    strOutput = FRAME_BEGIN & vbCrLf
    nChars = Len(strBase64)
    iOffset = 1
    Do While nChars > FRAME_LEN
        strOutput = strOutput & Mid$(strBase64, iOffset, FRAME_LEN) & vbCrLf
        nChars = nChars - FRAME_LEN
        iOffset = iOffset + FRAME_LEN
    Loop
    ' -- append final line, if any
    If nChars > 0 Then
        strOutput = strOutput & Mid$(strBase64, iOffset, nChars) & vbCrLf
    End If
    strOutput = strOutput & FRAME_END & vbCrLf
    
    ' OUTPUT the MySecret-formatted string
    MySecret_Encrypt = strOutput

Clean_Up:
    Call WIPE_Data(abPlain(0), nPlain)
    Call WIPE_Data(abCompr(0), nCompr)
    Call WIPE_Data(abPad(0), nPad)
    Call WIPE_Data(abBlock(0), nBlock)
    Call WIPE_Data(abKey(0), nKey)
    Call WIPE_Data(abEncode(0), nEncode)
    Call WIPE_String(strBase64, Len(strBase64))
    Call WIPE_String(strOutput, Len(strOutput))
    
End Function


Public Function MySecret_Decrypt(strInput As String, strPassword As String) As String
' INPUT: password, string of MySecret v3 base64-encoded ciphertext
' OUTPUT: decrypted plaintext string or empty string on error
    
    Dim strBase64 As String
    Dim nBeg As Long
    Dim nEnd As Long
    Dim i As Long
    Dim iOffset As Long
    Dim nRet As Long
    Dim abEncode() As Byte
    Dim nEncode As Long
    Dim abInitVec() As Byte
    Dim abKey() As Byte
    Dim nKey As Long
    Dim abBlock() As Byte
    Dim nBlock As Long
    Dim abCompr() As Byte
    Dim nCompr As Long
    Dim abPlain() As Byte
    Dim nPlain As Long
    Dim nPad As Long
    Dim nCrc24 As Long
    Dim nCrcChk As Long
    
    If Len(strInput) = 0 Then
        Debug.Print "ERROR: nothing to decrypt!"
        Exit Function
    End If
    
    ' Pre-set byte arrays in case we fail - see Clean_Up
    abEncode = StrConv("", vbFromUnicode)
    abKey = StrConv("", vbFromUnicode)
    abBlock = StrConv("", vbFromUnicode)
    abCompr = StrConv("", vbFromUnicode)
    abPlain = StrConv("", vbFromUnicode)
    
    Debug.Print "Input = " & Len(strInput) & " bytes"
    ' REMOVE FRAMING, get base64 data
    nBeg = InStr(1, strInput, FRAME_STUB)
    If nBeg <= 0 Then
        Debug.Print "ERROR: Not valid MySecret data!"
        Exit Function
    End If
    nBeg = nBeg + Len(FRAME_STUB)
    ' -- skip to start of base64 data (old-style headers may be "BEGIN MYSECRETxxx-----")
    nBeg = InStr(nBeg, strInput, "-")
    Do While nBeg > 0 And nBeg < Len(strInput) And Mid(strInput, nBeg, 1) = "-"
        nBeg = nBeg + 1
    Loop
    If nBeg >= Len(strInput) Then
        Debug.Print "ERROR: Not valid MySecret data!"
        Exit Function
    End If
    nEnd = InStr(nBeg, strInput, FRAME_END)
    If nEnd <= 0 Or nEnd <= nBeg Then
        Debug.Print "ERROR: Not valid MySecret data!"
        Exit Function
    End If
    strBase64 = Mid$(strInput, nBeg, nEnd - nBeg)
    Debug.Print "Base64=" & strBase64
    
    ' DECODE BASE64 to byte array (any non-base-64 chars, including CR-LFs, are ignored)
    abEncode = cnvBytesFromB64Str(strBase64)
    nEncode = UBound(abEncode) + 1
    Debug.Print "Encoded data = " & nEncode & " bytes"
    Debug.Print cnvHexStrFromBytes(abEncode)
    
    '+------+-------+-----------------------------------------------------------+
    '|SIG(4)| IV(8) | Ciphertext...                                             |
    '+------+-------+-----------------------------------------------------------+
    
    ' We expect 4 signature bytes 0x4D59FC00 and at least 15 bytes of data
    If nEncode < 15 Or abEncode(0) <> &H4D Or abEncode(1) <> &H59 Or abEncode(3) <> &H0 Then
        Debug.Print "ERROR: Not MySecret data."
        GoTo Clean_Up
    End If
    ' -- the third byte gives us the version: 0xFC=v3, 0xFD=v2
    If abEncode(2) <> &HFC Then
        Debug.Print "ERROR: Sorry, we only decrypt version 3."
        GoTo Clean_Up
    End If
    Debug.Print "Found MYSECRET signature for version 3"
    
    ' Copy the 8-byte IV
    ReDim abInitVec(BLOCK_LEN - 1)
    iOffset = 4
    For i = 0 To BLOCK_LEN - 1
        abInitVec(i) = abEncode(iOffset + i)
    Next
    Debug.Print "IV=" & cnvHexStrFromBytes(abInitVec)
    
    ' RE-CREATE THE KEY
    ' CREATE the 128-bit key using stretching with the IV as a salt
    abKey = MakeStretchedKey(strPassword, abInitVec, BLOCK_LEN, STRETCH_COUNT)
    Debug.Print "KEY=" & cnvHexStrFromBytes(abKey)
    nKey = UBound(abKey) + 1
    
    ' Copy the decryption block
    iOffset = BLOCK_LEN + 4
    nBlock = nEncode - iOffset
    ReDim abBlock(nBlock - 1)
    For i = 0 To nBlock - 1
        abBlock(i) = abEncode(iOffset + i)
    Next
    Debug.Print "Encr block (before)=" & cnvHexStrFromBytes(abBlock)
    
    ' DECRYPT the entire block
    nRet = BLF_BytesMode(abBlock(0), abBlock(0), nBlock, abKey(0), nKey, DECRYPT, "CBC", abInitVec(0))
    Debug.Print "BLF_BytesMode returns " & nRet & " (expecting 0)"
    If nRet <> 0 Then
        Debug.Print "ERROR: Decryption error."
        GoTo Clean_Up
    End If
    Debug.Print "Encr block (after) =" & cnvHexStrFromBytes(abBlock)
    
    ' PARSE the v3 block
    '|<------------------------nBlock--------------------------->|
    '|<--------10--------->|<------nCompr------>|<--3->|<-nPad-->|
    '+-----+-------+-------+--------------------+------+---------+
    '|ZS(2)|CLEN(4)|ULEN(4)| Compressed data... |CRC(3)|PAD(9-72)|
    '+-----+-------+-------+--------------------+------+---------+
    
    ' EXAMINE the decrypted block for 2-byte signature
    If abBlock(0) <> &H5A Or abBlock(1) <> &H4 Then
        Debug.Print "ERROR: Decryption error."
        GoTo Clean_Up
    End If
    
    ' EXTRACT lengths from big-endian-encoded values
    iOffset = 2
    nCompr = uwJoin(abBlock(iOffset + 0), abBlock(iOffset + 1), abBlock(iOffset + 2), abBlock(iOffset + 3))
    iOffset = 6
    nPlain = uwJoin(abBlock(iOffset + 0), abBlock(iOffset + 1), abBlock(iOffset + 2), abBlock(iOffset + 3))
    Debug.Print "Compressed length=" & nCompr & ", Uncompressed length=" & nPlain
    
    ' CHECK for reasonableness
    If nCompr < 0 Or nPlain < 0 Or nCompr > nBlock Then
        Debug.Print "ERROR: Decryption error."
        GoTo Clean_Up
    End If
    
    ' EXTRACT length of padding string: length is given by the very last byte.
    nPad = CLng(abBlock(nBlock - 1))
    Debug.Print "Padding string is " & nPad & " bytes"
    
    ' CONFIRM all lengths now match
    If nBlock <> 10 + nCompr + 3 + nPad Then
        Debug.Print "ERROR: Decryption error."
        GoTo Clean_Up
    End If
    
    ' CHECK GUARD BYTES: we expect 8 bytes of value NPAD at the start of the padding string
    iOffset = nBlock - nPad
    For i = 0 To 7
        If abBlock(iOffset + i) <> CByte(nPad) Then
            Debug.Print "ERROR: Decryption error."
            GoTo Clean_Up
        End If
    Next
    
    ' EXTRACT the 3-byte CRC-24 value immediately before
    iOffset = nBlock - nPad - 3
    ' -- note this is only 3-bytes long with a zero most-significant byte
    nCrcChk = uwJoin(&H0, abBlock(iOffset + 0), abBlock(iOffset + 1), abBlock(iOffset + 2))
    Debug.Print "CRC-24 value found = " & Hex(nCrcChk)
    
    ' DECOMPRESS the plaintext (careful to use exact lengths here)
    ReDim abCompr(nCompr - 1)
    ReDim abPlain(nPlain - 1)
    iOffset = 10
    For i = 0 To nCompr - 1
        abCompr(i) = abBlock(iOffset + i)
    Next
    nRet = ZLIB_Inflate(abPlain(0), nPlain, abCompr(0), nCompr)
    Debug.Print "ZLIB_Inflate returns " & nRet & " (expecting " & nPlain & ")"
    If nRet < 0 Then
        Debug.Print "ERROR: Decryption error (inflate failed)."
        GoTo Clean_Up
    End If
    Debug.Print "Decompressed data (hex)=" & cnvHexStrFromBytes(abPlain)
    
    ' COMPUTE the CRC-24 checksum for the recovered plaintext
    nCrc24 = CRC24_Bytes(abPlain)
    Debug.Print "CRC-24 = " & Hex(nCrc24)
    
    ' VERIFY that the checksums match
    If nCrc24 <> nCrcChk Then
        Debug.Print "ERROR: Decryption error (CRC checksum failed)."
        GoTo Clean_Up
    End If
    
    ' DECODE the byte array to an output string
    MySecret_Decrypt = StrConv(abPlain, vbUnicode)
        
Clean_Up:
    If UBound(abEncode) > 0 Then Call WIPE_Data(abEncode(0), nEncode)
    If UBound(abKey) > 0 Then Call WIPE_Data(abKey(0), nKey)
    If UBound(abBlock) > 0 Then Call WIPE_Data(abBlock(0), nBlock)
    If UBound(abCompr) > 0 Then Call WIPE_Data(abCompr(0), nCompr)
    If UBound(abPlain) > 0 Then Call WIPE_Data(abPlain(0), nPlain)
    
End Function

' ******************
' INTERNAL FUNCTIONS
' ******************

Private Function MakeStretchedKey(strPassword As String, abSalt() As Byte, nSalt As Long, _
    nStretchCount As Long) As Variant
' Returns a 128-bit/16-byte key in a byte array passed back as a VARIANT
    Dim abPassword() As Byte
    Dim abTemp() As Byte
    Dim abDigest() As Byte
    Dim nPassword As Long
    Dim nTemp As Long
    Dim nDigest As Long
    Dim i As Long
    Dim iCount As Long
    Dim iOffset As Long
    Dim abKey() As Byte
    Dim nKey As Long
    Dim nRet As Long
    
    ' INPUT:  password (p) and salt (s).
    ' OUTPUT: 16-byte key
        ' Set X(1) = MD5 (p || s)
        ' For i = 2 to 1024, set X(i) = MD5 (X(i-1) || p || s)
        ' Set the key as the final value of X(i).
        ' where || denotes ordered concatenation of two strings.
    ' COMMENT: this method is messy to do in VB. In retrospect, the PBKDF1 method from PKCS#5
    ' would have been easier and just as secure, but we used this variant (from Schneier,
    ' Applied Cryptography, p??) in Version 1 and so we stick with it.
    
    nKey = 16
    nDigest = API_MAX_MD5_BYTES
    ReDim abDigest(nDigest - 1)
    abPassword = StrConv(strPassword, vbFromUnicode)
    nPassword = UBound(abPassword) - LBound(abPassword) + 1
    ' Set X(1) = MD5 (p || s)
    nTemp = nPassword + nSalt
    ReDim abTemp(nTemp - 1)
    iOffset = 0
    For i = 0 To nPassword - 1
        abTemp(iOffset + i) = abPassword(i)
    Next
    iOffset = nPassword
    For i = 0 To nSalt - 1
        abTemp(iOffset + i) = abSalt(i)
    Next
    nRet = MD5_BytesHash(abDigest(0), abTemp(0), nTemp)
    
    ' For i = 2 to 1024, set X(i) = MD5 (X(i-1) || p || s)
    nTemp = nDigest + nPassword + nSalt
    ReDim abTemp(nTemp - 1)
    For iCount = 2 To nStretchCount
        iOffset = 0
        For i = 0 To nDigest - 1
            abTemp(iOffset + i) = abDigest(i)
        Next
        iOffset = iOffset + nDigest
        For i = 0 To nPassword - 1
            abTemp(iOffset + i) = abPassword(i)
        Next
        iOffset = iOffset + nPassword
        For i = 0 To nSalt - 1
            abTemp(iOffset + i) = abSalt(i)
        Next
        nRet = MD5_BytesHash(abDigest(0), abTemp(0), nTemp)
    Next iCount
    
    ' Set the key as the final value of X(i).
    If nKey > nDigest Then nKey = nDigest
    ReDim abKey(nKey - 1)
    For i = 0 To nKey - 1
        abKey(i) = abDigest(i)
    Next
    
    ' Return key as a Byte array in a Variant
    MakeStretchedKey = abKey
    
    ' Clean up
    Call WIPE_Data(abTemp(0), nTemp)
    Call WIPE_Data(abDigest(0), nDigest)
    Call WIPE_Data(abPassword(0), nPassword)
    Call WIPE_Data(abKey(0), nKey)

End Function

' ************************************************************************
' FUNCTIONS TO CONVERT BETWEEN 32-BIT INTEGERS AND BIG-ENDIAN BYTES ARRAYS
' ************************************************************************
Private Function BytesFromLong(ByVal w As Long) As Variant
' Returns a byte array but as a VARIANT type
    Dim abBytes() As Byte
    ReDim abBytes(3)
    Call uwSplit(w, abBytes(0), abBytes(1), abBytes(2), abBytes(3))
    BytesFromLong = abBytes
End Function

Private Function uwJoin(a As Byte, b As Byte, c As Byte, d As Byte) As Long
' Join 4 x 8-bit bytes into one 32-bit word a.b.c.d
    uwJoin = ((a And &H7F) * &H1000000) Or (b * &H10000) Or (CLng(c) * &H100) Or d
    If a And &H80 Then
        uwJoin = uwJoin Or &H80000000
    End If
End Function

Private Sub uwSplit(ByVal w As Long, a As Byte, b As Byte, c As Byte, d As Byte)
' Split 32-bit word w into 4 x 8-bit bytes
    a = CByte(((w And &HFF000000) \ &H1000000) And &HFF)
    b = CByte(((w And &HFF0000) \ &H10000) And &HFF)
    c = CByte(((w And &HFF00) \ &H100) And &HFF)
    d = CByte((w And &HFF) And &HFF)
End Sub

' **************************************************
' FUNCTIONS TO CARRY OUT CRC-24 CHECKSUM COMPUTATION
' **************************************************
' basCRC24: Calculates CRC-24 checksum for a given message string
' Version 1. Published 4 June 2003.
'************************* COPYRIGHT NOTICE*************************
' This code was originally written in Visual Basic by David Ireland
' and is copyright (c) 2003-6 D.I. Management Services Pty Limited,
' all rights reserved.
' For more details, see <http://www.di-mgt.com.au/src/basCRC24.txt>
'*******************************************************************
Public Function CRC24_Bytes(abMessage() As Byte) As Long
    Const CRC24_INIT As Long = &HB704CE
    Const CRC24_POLY As Long = &H1864CFB
    Dim i As Long
    Dim j As Integer
    Dim ulCRC As Long
    
    ulCRC = CRC24_INIT
    For i = LBound(abMessage) To UBound(abMessage)
        ulCRC = ulCRC Xor ulShiftLeftBy16(abMessage(i))
        For j = 0 To 7
            ulCRC = ulShiftLeftByOne(ulCRC)
            If (ulCRC And &H1000000) <> 0 Then
                ulCRC = ulCRC Xor CRC24_POLY
            End If
        Next
    Next
    
    CRC24_Bytes = ulCRC And &HFFFFFF

End Function

Public Function CRC24_String(sMessage As String) As Long

    Dim abMessage() As Byte
    
    ' Use proper VB function to get an array of bytes
    ' thus avoiding problems with Unicode/ANSI/DBCS character sets
    abMessage = StrConv(sMessage, vbFromUnicode)
    CRC24_String = CRC24_Bytes(abMessage)
    
End Function

Private Function ulShiftLeftBy16(ByVal wordX As Long) As Long
' Shift 32-bit long value to left by 16 bits
' i.e. VB equivalent of "wordX << 16" in C
' Avoiding problem with sign bit
' Copyright (C) 2000-03 DI Management Services Pty Ltd
    ulShiftLeftBy16 = (wordX And &H7FFF&) * &H10000
    If (wordX And &H8000&) <> 0 Then
        ulShiftLeftBy16 = ulShiftLeftBy16 Or &H80000000
    End If
End Function

Private Function ulShiftLeftByOne(ByVal wordX As Long) As Long
' Shift 32-bit long value to left by 1 bits
' i.e. VB equivalent of "wordX << 1" in C
' Avoiding problem with sign bit
' Copyright (C) 2000-03 DI Management Services Pty Ltd
    ulShiftLeftByOne = (wordX And &H7FFFFFFF) * &H2
    If (wordX And &H8000000) <> 0 Then
        ulShiftLeftByOne = ulShiftLeftByOne Or &H80000000
    End If
End Function