Both HashObject and Symmetic
objects have similar usage patterns. Any differences come from the
fact that the first (HashObject) only inspects the data to generate a
hash or HMAC digest while the second (Symmetric) encrypts the data and
you get a cipher as a result. So, there is an easy way and a hard way
for the both of them. The easy way needs minimal code but may cost
memory consumption, while the hard way enables you to split the
process of hashing or encryption/decryption in chunks at the cost of a
little more coding.
The easy way - hash/en/de/crypt in single step
The hard way - hash/en/de/crypt in chunks
The block size - what you need to take care of when using symmetric
algoruthms.
The Easy Way
Lets begin with the HashObject. You need to configure it - set the
algorithm, a key if HMAC is to be generated and use it. In the
examples below we will illustrate the usage in a scenario where a file
is processed. This gives a nice and natural base for comparison
between the different kinds of usage. Still, the same general usage
pattern will occur with other data - from a database, from network
connection and so on.
Set ho = Server.CreateObject("newObjects.crypt.HashObject")
' In NSBasic this would be:
' AddObject "newObjects.crypt.HashObject", ho
' in other environments the way you create the object may differ - check their
' documentation about how COM objects are created.
ho.InitHash "MD5" ' select MD5 algorithm
' Now open a file
Set sf = Server.CreateObject("newObjects.utilctls.SFMain")
Set file = sf.OpenFile("somefile",&H40) ' open it for reading only
' Now read all the file and hash it
binFileData = file.ReadBin(file.Size)
ho.HashData binFileData
' What remains is to get the hash
hash = ho.Value ' If we want it as hexdecimal string
' hash = ho.BinaryValue ' if we want it as binary data
...
We read the file in binary mode (by using ReadBin and not ReadText).
This is usually good for any kind of file. Sometimes the data must be
processed as text explicitly. Then we need to know or establish a rule
about how to convert it before processing. Why? Imagine you want to
hash a text file - it can be in UNICODE format, but it can be in a
singlebyte/multibyte encoding as well. Now the other party that will
verify the hash may want to keep the data not in the format you have
it - for example save it as ANSI (singlebyte encoding - the typical
format for a text file in Windows) while you have it as UNICODE (for
example directly obtained from a database query). Obviously the hash
generated over the UNICODE text data will differ from the hash
generated over the ANSI text data, because on a binary level they are
different. So you need to impose a rule or obey an existing one when
generating the hash. This explains the second parameter of the
HashData method - code page.
How this looks with symmetric encryption? The major difference is
that you will not only pass data but also get encrypted/decrypted data
back and do something with it. To illustrate this we will read one
file encrypt it and write the encrypted data to another file:
Set cr = Server.CreateObject("newObjects.crypt.Symmetric")
' In NSBasic this would be:
' AddObject "newObjects.crypt.Symmetric", cr
' in other environments the way you create the object may differ - check their
' documentation about how COM objects are created.
cr.Init "DES" ' select DES algorithm
' Set a key
cr.Key = "0123456789ABCDEF"
' Configure padding
cr.PadType = -1 ' Select random bytes padding
' Now open a file
Set sf = Server.CreateObject("newObjects.utilctls.SFMain")
Set file = sf.OpenFile("somefile",&H40) ' open it for reading only
' Remember the file size
originalFileSize = file.Size
' Now read all the file and hash it
binFileData = file.ReadBin(file.Size)
cipher = cr.Encrypt(binFileData)
' What remains is to save/send somewhere the cipher
' Note! The file size we recorded above is important!
' The size of the cipher is most probably not the same
' as the size of the original data because of the algorithm's
' block size. Thus when we decrypt we will need to know
' the original size and truncate the result.
...
Note the comments about the block size and the need to know the
size of the original data. Most symmetric algorithms process data in
blocks and respectively the cipher they produce is not the same size
as the original, but is multiple of the algorithm's block size. You
may ask - "well but there are components and API-s that offer
encryption/decryption without additional parameters". To achieve
this you need a convention that will pack additional data with the
data you want to encrypt. There are standards about that, but there
are also many exceptions, custom implementations and even
alternate standards. With HashCryptStreams library you can implement
any of them or devise your own scheme. As you may already guess the
simplest method is to encrypt the file size with he file contents -
most convenient is to put the size in a fixed size field before the
file contents. An example doing that is shown in the next section.
The decryption is actually much the same - you just use the Decrypt
method instead of Encrypt and keep in mind that the cipher's size is
multiple of the block size and not the size of the original
data.
The Hard Way
When the data is too much using the one-call approach will cost too
much memory. So, the obvious way to go is performing the operation in
multiple steps passing the data in chunks. In HashObject this is done
through the HashUpdate and HashFinalize methods. The first one passes
a chunk of data for processing and HashFinalize is called once - after
the last chunk of data. The example from above re-written to use this
approach will look like this:
Set ho = Server.CreateObject("newObjects.crypt.HashObject")
' In NSBasic this would be:
' AddObject "newObjects.crypt.HashObject", ho
' in other environments the way you create the object may differ - check their
' documentation about how COM objects are created.
ho.InitHash "MD5" ' select MD5 algorithm
' Now open a file
Set sf = Server.CreateObject("newObjects.utilctls.SFMain")
Set file = sf.OpenFile("somefile",&H40) ' open it for reading only
' Now read the file in a cycle and hash it
While Not file.EOS
binFileData = file.ReadBin(256)
ho.HashUpdate binFileData
Wend
ho.HashFinalize
' What remains is to get the hash
hash = ho.Value ' If we want it as hexdecimal string
' hash = ho.BinaryValue ' if we want it as binary data
...
So, we read 256 bytes from the file each turn and pass it to the
HashObject. Finally when everything is read we call HashFinalize once
and we have the digest in the same properties as before.
When a new HashUpdate/HashFinalize can be started? When you need to
perform the operation several times over different data you will need
to know how to reuse the object (i.e. perform the same without need to
re-create it again). It is simple - just call Reset and start again.
If the operation you have performed before is HMAC and you want to get
rid of the key (i.e. the next operation will be hash and not HMAC) you
call ResetKey instead.
Now lets do the same with the Symmetric
encryption object. In order to illustrate a sensible usage we will
also include the data size with the data passed for encryption and
then recover it when decrypting.
Encryption
We assume that some of the variables (such as the file names,
encryption keys etc.) are already initialized with appropriate
values.
' Create the objects we need
Set crypt = CreateObject("newObjects.crypt.Symmetric")
Set sf = CreateObject("newObjects.utilctls.SFMain")
Set sfbd = CreateObject("newObjects.utilctls.SFBinaryData")
' Open the files
Set infile = sf.OpenFile(infileName,&H40)
Set outfile = sf.CreateFile(outfileName)
' Init the Symmetric encryption object
crypt.Init alg ' alg is a string - the name of one of the supported algorithms
crypt.Key = key ' is a key appropriate for the selected algorithm
crypt.PadType = -1
' We use 4 byte field to hold an long integer (4 byte) number
' So allocate 4 bytes
sfbd.Size = 4
' Set a big endian byte order (not that little endian is bad ;)
sfbd.ByteOrder = &H02
sfbd.Unit(0,vbLong) = infile.Size
' Write it first in the output
outfile.WriteBin crypt.Encrypt(sfbd.Value,False)
' Encrypt the file
While Not infile.EOS
chunk = infile.ReadBin(256)
If Not infile.EOS Then
cipher = crypt.Encrypt(chunk,False)
Else
cipher = crypt.Encrypt(chunk,True)
End If
' Write to the output file
outfile.WriteBin cipher
Wend
' We are done - we can close the files and move on
This example shows pretty well what kind of other components are
often needed when working with the HashCryptStreams library. The SFBinaryData
object is one of those that you will almost always need. It provides
advanced manipulation of binary data blocks, with regard to the byte
order and the field sizes. When you need to follow a standard or catch
up with another application that uses its own custom encryption scheme
you will need to prepare or/and read the header data packed with the
actual data. Almost always this will be a binary data block with some
fields with certain sizes. For example a field for data size, a field
for a file name (with limited size) and so on.
Note that the objects from the HashCryptStreams library seemingly
support some of the operations provided by the SFBinaryData object.
However there is no point in duplicating its features in every object
that may need them. Therefore the features supported by the
HashCryptStream objects are limited to the minimal set that would make
their usage convenient by enabling you to init them with already
prepared data. They do not support themselves the details you may need
in the most real world situations (for example there is no byte order
control in them, nor bit-wise operations over the data). Thus the
SFBinaryData is a must have object in almost any encryption related
code. The above example is very basic, it is very likely that you
would want to pack more metadata with the encrypted content, some
standards or specific application requirements will need even more
complicated operations. When you are building code that works with
them you will need to obey those requirements and a binary buffer
manipulation is something you would need.
What about the arrays or the strings in VBScript, NSBasic? You may
know some examples that use them to deal with binary data and be
tempted to go that way. The problem is that the high level languages
are type-less and even if the VB like ones offer some ways to convert
the data to the right type these features are not complete and cover
only part of the tasks you will need to perform. Furthermore the
chance to make a mistake using type-less tools is very high and often
hard to find. For example an array defined as Dim arr(10) is array of
variants, no matter if you assign bytes to its elements. Nothing would
prevent the application from assigning to some elements other data
(for example string or a floating point number). Using ChrB/MidB/AscB
and the other "B" functions in VBScript may look promising,
but you will get in trouble when you need to deal with bigger than
byte fields and use a byte order that is not native for the operating
system.
After commenting all these details lets return to the example and
perform the reverse operation - decryption.
Decryption
' Create the objects we need
Set crypt = CreateObject("newObjects.crypt.Symmetric")
Set sf = CreateObject("newObjects.utilctls.SFMain")
Set sfbd = CreateObject("newObjects.utilctls.SFBinaryData")
' Open the files
Set infile = sf.OpenFile(infileName,&H40)
Set outfile = sf.CreateFile(outfileName)
' Init the Symmetric encryption object
crypt.Init alg
crypt.Key = key
' We will decrypt only - so there is no need to care about the padding method this time.
' The encrypted file contains the original file size in its first 4 bytes
' However we cannot read just 4 bytes because the block size can be bigger
' (usually is). So, to ensure we need to read entire block in a memory stream.
Set mem = sf.CreateMemoryStream
' How much to read, better read more than needed than less
blocksToRead = (crypt.BlockSize / 4) + 1
mem.WriteBin crypt.Decrypt(infile.ReadBin(crypt.BlockSize),False)
' Why memory stream? Well, you can use SFBinaryData for everything.
' we just used that in this example to make the task easy to understand
' Now use the binary data object to consume the size
sfbd.Size = 4
mem.Pos = 0
sfbd.Value = mem.ReadBin(4)
' Now get what we need and write the rest to the output
sfbd.ByteOrder = &H02
FileSize = sfbd.Unit(0,vbLong)
' Write the rest of the block to the output
mem.CopyTo outfile, crypt.BlockSize ' The number of bytes just needs to be bigger than the actual content
' mem is no longer needed lets dispose of it explicitly (not actually needed,
' it will be freed as the script completes anyway.
Set mem = Nothing
' Decrypt the rest
Do
chunk = infile.ReadBin(256)
If Not infile.EOS Then
bindata = crypt.Decrypt(chunk,False)
Else
bindata = crypt.Decrypt(chunk,True)
End If
' Write to the output file
outfile.WriteBin bindata
Loop While Not infile.EOS
' Truncate the output file to the size we read before
outfile.Size = FileSize
If you think the above code is too long for such a task - remember
we are doing everything on our own here. Typically you will pack such
tasks in functions and call them as needed. If there were only
predefined en/de-cryption oprations doing this or something else
implicitly you would not have the chance to handle encrypted data that
encodes the additional information in other manner than the built-in
one.
|