=begin
Digest r1
by Franc[e]sco
01/07/2015
r0
[+] Initial revision
01/08/2015
r1
[*] Better commenting
=end
# Win32API
GetLastError = Win32API.new("Kernel32", "GetLastError", "", "I")
CryptAcquireContext = Win32API.new("Advapi32",
"CryptAcquireContext", "PPPII", "I")
CryptCreateHash = Win32API.new("Advapi32", "CryptCreateHash", "PIPIP", "I")
CryptHashData = Win32API.new("Advapi32", "CryptHashData", "PPII", "I")
CryptImportKey = Win32API.new("Advapi32", "CryptImportKey", "PPIPIP", "I")
CryptSetHashParam = Win32API.new("Advapi32", "CryptSetHashParam", "PIPI", "I")
CryptGetHashParam = Win32API.new("Advapi32", "CryptGetHashParam", "PIPPI", "I")
CryptDestroyHash = Win32API.new("Advapi32", "CryptDestroyHash", "P", "I")
CryptDestroyKey = Win32API.new("Advapi32", "CryptDestroyKey", "P", "I")
CryptReleaseContext = Win32API.new("Advapi32", "CryptReleaseContext", "P", "I")
# various utilities for hashing
module Digest
# generates a HMAC-SHA1
#
# keybytes: the key (string)
# data: the data that will be digested
#
# returns the array [hmac, last_error]
# if successful, hmac contains the HMAC-SHA1 and last_error is zero
# if unsuccessful, hmac contains the error message and last_error
# contains the value of GetLastError after the error happened.
def self.hmacsha1(keybytes, data)
provider = 0 # handle to the crypto service provider
hash = 0 # handle to a hash object
key = 0 # handle to a symmetric key
hmachash = 0 # handle to a HMAC hash
hashbytes = "" # the hash as a bynary string
datalen = 0 # hash length in bytes
errortext = nil # will be used to store errors
gle = 0 # will be use to store the value of GetLastError
=begin
RPGMaker's Ruby is extremely minimal, so the only way to call
WinAPIs that work on Win32 structs is to manually create a byte array
with the same size of the struct and manually pack the values to
recreate the memory region that would hold the struct.
We will basically create an array that contains the values of each
member of the struct in order and then use .pack and specify the
size of each element so that it will properly get packed.
Here's some of .pack's size specifiers:
C = uint8_t / BYTE
S = uint16_t / WORD
L = uint32_t / DWORD
c = int8_t
s = int16_t
l = int32_t
so for example this is how you pack a Win32 POINT struct with X=13,Y=37:
- Google "msdn POINT" to find the struct definition:
typedef struct tagPOINT {
LONG x;
LONG y;
} POINT, *PPOINT;
- x and y and LONGs
- LONG is a signed DWORD (int32_t), so the size specifier will be l
- Initialize an array with each member's value in order
mypoint = [13, 37]
- Before you pass it to a WinAPI, pack it and then unpack it after the call
to retrieve the new values:
packedpoint = mypoint.pack("ll") # LONG, LONG
some_winapi.call(packedpoint) # some_winapi expects a pointer to POINT
mypoint = packedpoint.unpack("ll") # retrieve the values of mypoint after
# some_winapi modified it
These are the sizes of the HMAC_INFO struct that I will be using
typedef struct _HMAC_Info {
ALG_ID HashAlgid; // L
BYTE *pbInnerString; // C
DWORD cbInnerString; // L
BYTE *pbOuterString; // C
DWORD cbOuterString; // L
} HMAC_INFO, *PHMAC_INFO;
=end
hmacinfo = [0, 0, 0, 0, 0] # zeroed HMAC_INFO struct
hmacinfo[0] = 0x00008004 # hmacinfo.HashAlgid = CALG_SHA1
# this is a fake loop that breaks after 1 iteration
# I break out of it on errors to jump straight to the cleanup code
loop do
# acquire a handle to the default RSA crypto service provider
#
# PROV_RSA_FULL = 1
# CRYPT_VERIFYCONTEXT = 0xF0000000
if not CryptAcquireContext.call(o=[provider].pack('i!'),
0, 0, 1, 0xF0000000)
gle = GetLastError.call
errortext = sprintf("CryptAcquireContext failed")
break
end
provider = o.unpack('i!')[0]
=begin
by the way, packing pointers to integers for winapi's works similarly
to structs, as you see in the call above
my_value = 0
packed_value = [my_value].pack('i!')
some_winapi(packed_value)
my_value = packed_value.unpack('i!')[0]
=end
# acquire a handle to a new hash object
#
# CALG_SHA1 = 0x00008004
if not CryptCreateHash.call(provider, 0x00008004, 0, 0,
o=[hash].pack('i!'))
gle = GetLastError.call
errortext = "CryptCreateHash CALG_SHA1 failed"
break
end
hash = o.unpack('i!')[0]
# hash the key
if not CryptHashData.call(hash, keybytes, keybytes.length, 0)
gle = GetLastError.call
errortext = "CryptHashData CALG_SHA1 failed"
break
end
=begin
typedef struct _PUBLICKEYSTRUC {
BYTE bType; // C
BYTE bVersion; // C
WORD reserved; // S
ALG_ID aiKeyAlg; // L
} BLOBHEADER, PUBLICKEYSTRUC;
struct {
BLOBHEADER hdr;
DWORD len; // L
BYTE key[1024]; // C * 1024
} key_blob;
embedded structs are the same as replacing that embedded struct
with the struct's members.
therefore, the packing for key_blob is: "CCSLL" + "C" * 1024
- CCSL is the embedded BLOBHEADER
- L is len
- and then we have 1024 C's which are the bytes of key
=end
# This is what you'd normally do in C
# key_blob.hdr.bType = PLAINTEXTKEYBLOB;
# key_blob.hdr.bVersion = CUR_BLOB_VERSION;
# key_blob.hdr.reserved = 0;
# key_blob.hdr.aiKeyAlg = CALG_RC2;
# key_blob.len = keyBytes.size();
#
# This is how we do it with packed structs in Ruby
key_blob = [
8,
2,
0,
0x00006602,
keybytes.length
]
# PLAINTEXTKEYBLOB = 8
# CUR_BLOB_VERSION = 2
# CALG_RC2 = 0x00006602
# initialize zeroed memory for "key"
for i in 1..1024
key_blob << 0
end
blobpacking = "CCSLL" + "C" * 1024
# copy keybytes to key_blob.key
i = 0
keybytes.each_byte do |kb|
key_blob[5 + i] = kb
i += 1
end
# derive the final key object from the key_blob struct
#
# CRYPT_IPSEC_HMAC_KEY = 0x00000100
if not CryptImportKey.call(provider,
packedblob=key_blob.pack(blobpacking),
key_blob.length, 0, 0x00000100, o=[key].pack('i!'))
gle = GetLastError.call
errortext = "CryptImportKey failed"
break
end
key = o.unpack('i!')[0]
key_blob = packedblob.unpack(blobpacking)
# acquire a handle to a new hash object
#
# CALG_HMAC = 0x00008009
if not CryptCreateHash.call(provider, 0x00008009, key,
0, o=[hmachash].pack('i!'))
gle = GetLastError.call
errortext = "CryptCreateHash CALG_HMAC failed"
break
end
hmachash = o.unpack('i!')[0]
# set the HMAC_INFO struct instance for the hash
#
# HP_HMAC_INFO = 0x0005
if not CryptSetHashParam.call(hmachash, 0x0005, o=hmacinfo.pack('LCLCL'), 0)
gle = GetLastError.call
errortext = "CryptSetHashParam failed"
break
end
hmacinfo = o.unpack('LCLCL')
# hash the message/data
if not CryptHashData.call(hmachash, data, data.length, 0)
gle = GetLastError.call
errortext = "CryptHashData CALG_HMAC failed"
break
end
# get hash length in bytes for the HMAC
#
# HP_HASHVAL = 0x0002
if not CryptGetHashParam.call(hmachash,
0x0002, 0, o=[datalen].pack('i!'), 0)
gle = GetLastError.call
errortext = "CryptGetHashParam 1 failed"
break
end
datalen = o.unpack('i!')[0]
# allocate an array that will hold the final HMAC
hashbytes = "\0" * datalen
# store the HMAC into hashbytes
#
# HP_HASHVAL = 0x0002
if not CryptGetHashParam.call(hmachash, 0x0002, hashbytes,
o=[datalen].pack('i!'), 0)
gle = GetLastError.call
errortext = "CryptGetHashParam 2 failed"
break
end
break
end
# cleanup
CryptDestroyHash.call(hmachash) if hmachash
CryptDestroyKey.call(key) if key
CryptDestroyHash.call(hash) if hash
CryptReleaseContext.call(provider) if provider
# return errortext instead of the hash if an error has occurred
if gle != 0
return [errortext, gle]
end
return [hashbytes, gle]
end
end