Procedures identification

Here is a module “Invisible Basic”, writen by John C. Gunther, that identifies procedures in modules. From there on, they can be ported.
' Invisible Basic: A utility for the obfuscation of VBA code
' in Excel Workbooks. See the Invisible Basic User's Guide
' (InvisibleBasic.html) for a detailed description of why
' this is useful.
' Copyright (c) 2005, John C. Gunther.
' All rights reserved.
' Redistribution and use in source and binary forms, with
' or without modification, are permitted provided that the
' following conditions are met:
' - Redistributions of source code must retain the above
' copyright notice, this list of conditions and the following
' disclaimer.
' - Redistributions in binary form must reproduce the above
' copyright notice, this list of conditions and the following
' disclaimer in the documentation and/or other materials
' provided with the distribution
' - Neither the name of the Invisible Basic Consortium nor
' the names of its contributors may be used to endorse or
' promote products derived from this software without
' specific prior written permission.
' Note: the licence conditions above were copied from the
' BSD open source license template available at

Option Explicit

Public Const INVISIBLE_BASIC_VERSION As Double = 3#
Public Const INVISIBLE_BASIC_URL As String = ""

' Token types: as a program's source code is scanned, it is
' separated into a stream of tokens each with one of these
' types:
Private Const TT_IDENTIFIER As Integer = 1 'variable names, keywords, etc.
Private Const TT_STRING As Integer = 2 ' string literal ("myString")
Private Const TT_NUMBER As Integer = 3 ' numerical literal (1.23)
Private Const TT_COMMENT As Integer = 4 ' VBA comment text
Private Const TT_WHITESPACE As Integer = 5 ' space or tab
Private Const TT_GUID As Integer = 6 ' global universal identifier
' ({C62A69F0-16DC-11CE-9E98-00AA00574A4F})
Private Const TT_OTHER As Integer = 7 ' everything else

' Name of the file that contains the list of visible (not obfuscated)
' Excel/VBA keywords, reserved Excel object model names, etc.
Private Const IB_VISIBLE_KEYWORDS_FILENAME = "visible_names.txt"

' establish classes of characters helpful in tokenization:

Private Const alphaChars As String = "abcdefghijklmnopqrstuvwxyz"
Private Const underscore As String = "_"
Private Const digits As String = "0123456789"
Private Const dQuote As String = """"
Private Const GUID_START As String = "{" ' "Global Universal ID"
Private Const GUID_END As String = "}" ' (occurs in UserForm headers)
' Note: by including underscore as whitespace, parsing of continued
' lines (ending in " _") is facilitated. VBA does not allow identifiers to begin with
' underscores, so this does not cause ambiguities with the lexical
' analysis of identifiers.
Private Const wsChars As String = " " & vbTab & vbNewLine & underscore
Private Const firstNumericChars As String = digits
Private Const numericChars As String = firstNumericChars & "."
Private Const firstCommentChar As String = "'"
Private Const doubleComment As String = firstCommentChar & firstCommentChar
Private Const line_continuation_chars As String = " " & underscore
Private Const firstIdentifierChars As String = alphaChars
Private Const identifierChars As String = alphaChars & underscore & digits
' e.g. in the event procedure myButton_Click, "_" delimits the
' control name from the event name:
Private Const userform_event_delimiter As String = underscore
' if this character preceeds an indentifier within visible_names.txt, it flags
' that identifier as a userform control attribute.
Private Const userform_control_attribute_flag As String = underscore
Private Const object_attribute_delimiter As String = "." 'object attribute delimiter (e.g. the "." in myLabel.Caption)

' These keywords, when encountered in source code, are
' recognized by Invisible Basic as directives that define if
' identifiers will be obfucated ("invisible") or retained as is
' ("visible")

Private Const VISIBLE_KEYWORD As String = "#visible" ' for single lines
' for delimiting visible blocks:
Private Const BEGIN_VISIBLE_KEYWORD As String = "#begin_visible"
Private Const END_VISIBLE_KEYWORD As String = "#end_visible"

' this is added to the end of the workbook file name to get
' the default new, obfuscated, workbook's filename (e.g.
' myWorkbook.xls becomes saved invisibly, if user accepts
' the initial default name, as myWorkbook_ib.xls):

Private Const IB_FILENAME_SUFFIX As String = "_ib.xls"
' invisible basic overwrites this file without confirmation, so using
' the .tmp (temporary file) file type is essential.
Private Const IB_SECRET_DECODER_SUFFIX As String = "_secretDecoder.tmp"

' depth of #begin_visible ... #end_visible nesting:
Private m_visible_depth As Long

' returned when a specified identifier isn't found:
Private Const NO_SUCH_ID As String = ""

' lists of names that will remain in original format
' (visible), and of those that will be obuscated (made invisible)
Private visible_names As New Collection
Private invisible_names As New Collection

' lists of userform attribute names; userform attributes are used to
' identify userform control names either 1) via their use in event
' procedure names (e.g. the Click attribute identifies myButton as a
' control name in the event procedure myButton_Click) or 2) via the
' direct use of the attribute in code (e.g., the Caption attribute
' identifies myLabel as a control name in the code line:
' myLabel.Caption = "myLable Caption"). Such control names are
' automatically declared as "visible names" by Invisible Basic,
' and not obfuscated.
' Why do we even need this, you ask. Unlike most VBA variables,
' UserForm control names are NOT defined in the source code; because I
' could not figure out how to change these (non-source defined) names
' programmatically, I instead must be sure they are NOT changed in the
' source code (or else names would get out of synch, breaking the
' UserForm). Hence the need for these special "visible attribute"
' rules to recognize such control names.
' Pre 2.0 versions didn't have this feature, and thus required manual
' user intervention to declare such control names visible.

Private userform_attribute_names As New Collection

' if True, each obfuscated final code line will be preceeded with
' a comment containing the original, unobfuscated, line
' it came from (for trouble-shooting).

Private m_interleave_original_code_as_comments As Boolean

Private Const IB_NameOfInvisibleBasicMenu As String = "Invisible&Basic"
' Excel menu on which the Invisible Basic menu is placed:
Private Const IB_NameOfExcelWorksheetMenubar As String = _
"Worksheet Menu Bar"

Private Const IB_TEMP_FILENAME_PREFIX = "InvBas_Temp_" ' example temp filename: InvBas_Temp_1.tmp

' these declarations are used only by the visit_url function,
' used by the help command to open the Help file. Help command
' only works on Windows platforms.

Private Declare Function GetDesktopWindow Lib "user32" () As Long

Private Declare Function ShellExecute Lib "shell32" _
Alias "ShellExecuteA" _
(ByVal hwnd As Long, _
ByVal lpOperation As String, _
ByVal lpFile As String, _
ByVal lpParameters As String, _
ByVal lpDirectory As String, _
ByVal nShowCmd As Long) As Long

Private Const SW_SHOWNORMAL As Long = 1
Private Const SW_SHOWMAXIMIZED As Long = 3
Private Const SW_SHOWDEFAULT As Long = 10

' end of declarations for visit_url

' These constants (sans the "IB_" prefix) are defined in the
' Microsoft Visual Basic for Applications Extensibility Library.

' So that users do not have to add a reference to that library, we
' define copies of those constants here. Another reason for doing this
' is that there can be more than one of these libraries on a single
' machine, and if a reference to the wrong version is employed, the
' code breaks with a rather cryptic "type mismatch" error (the type of
' VBComponent used by Excel differs from the type used by the
' VBComponent in the Extensibility library if the extensibility
' library is from a newer version of Excel)

' If (as seems very unlikely) Microsoft ever changes these constants,
' these lines would have to be changed.

Private Const IB_vbext_ct_StdModule As Long = 1
Private Const IB_vbext_ct_ClassModule As Long = 2
Private Const IB_vbext_ct_MSForm As Long = 3
Private Const IB_vbext_ct_Document As Long = 100

' End of Microsoft VBA Extensibility library constants

' circular buffer of recently seen tokens (facilitates
' recognition of userform control names):
Private Const N_BUFFERED_TOKENS As Integer = 3
Private prevTokens(0 To N_BUFFERED_TOKENS - 1) As String
Private tokenIndex As Integer

' These are used for encoding name, value pairs into VB collections

Private Const NAME_ID As Integer = 1 ' represent the offsets into an array storing name, value pair
Private Const VALUE_ID As Integer = 2 ' (VB collections will not allow you to store user defined types)

Private Function PS() As String ' e.g. a "\" on Windows
PS = Application.PathSeparator
End Function

Public Property Get interleave_original_code_as_comments() As Boolean
interleave_original_code_as_comments = m_interleave_original_code_as_comments
End Property

Public Property Let interleave_original_code_as_comments(original_code_as_comments As Boolean)
m_interleave_original_code_as_comments = original_code_as_comments
End Property

' VBA within Excel 97 lacks "Debug.Assert". For compatibility with
' all major Excel versions, we therefore emulate it:
Private Sub assert(condition As Boolean)
If (Not condition) Then Stop
End Sub

' Does an old-style "Rem" comment begin at the specified position in the string?
' Note: Rem is a keyword, and thus cannot be used as a variable name
' in compilable VBA code--this fact simplifies the test below.
Private Function isRemComment(s As String, iStart As Integer) As Boolean
isRemComment = ("rem" = LCase(Trim(Mid(s, iStart, Len("rem ")))))
End Function

' Returns integer type of a token assumed to start at give
' position in the given string

' Note: Invisible Basic's lexical analysis is, by design, very simple
' and suitable only for this code obfuscation task. For example, the
' "E" in scientific notation numeric literals will be treated like a
' variable that is always visible (e.g. 1.2E10 is analyzed as the number
' 1.2, the always-visible name E, and the number 10). It all comes out OK in
' the end, but just be aware that the string of tokens seen by
' Invisible Basic is NOT the same as what Visual Basic sees.

Private Function token_type(s As String, _
iStart As Integer) As Integer
Dim c As String
Dim result As Integer
assert 1 <= iStart And iStart <= Len(s)
c = LCase(Mid(s, iStart, 1))
If (InStr(1, wsChars, c) <> 0) Then
ElseIf (firstCommentChar = c Or isRemComment(s, iStart)) Then
' this branch must preceed identifier branch or "Rem" comments will look like identifiers
result = TT_COMMENT
ElseIf (InStr(1, firstIdentifierChars, c) <> 0) Then
ElseIf (c = dQuote) Then
result = TT_STRING
ElseIf (c = GUID_START) Then
result = TT_GUID
ElseIf (InStr(1, firstNumericChars, c) <> 0) Then
result = TT_NUMBER
result = TT_OTHER
End If

token_type = result

End Function

' Returns the string position 1 character past the end of the
' token that starts at the given position in the given string.

Private Function end_of_token(s As String, _
iStart As Integer) As Integer
Dim iEnd As Integer
Dim tt As Integer
Dim matchChars As String
Dim invertMatch As Boolean
Dim matched As Boolean
Dim c As String

tt = token_type(s, iStart)

Select Case (tt)
matchChars = identifierChars
invertMatch = False
matchChars = dQuote
invertMatch = True ' all chars until next double quote
matchChars = GUID_END
invertMatch = True
matchChars = numericChars
invertMatch = False
matchChars = ""
invertMatch = True ' match everything until end of line
matchChars = wsChars
invertMatch = False
' any character that can NOT be viewed as the first char of
' one of the above token types
matchChars = firstIdentifierChars & firstNumericChars & _
firstCommentChar & dQuote & wsChars & GUID_START
invertMatch = True
End Select

iEnd = iStart + 1

Do While (iEnd <= Len(s))
c = LCase(Mid(s, iEnd, 1))
matched = InStr(1, matchChars, c) <> 0
If (invertMatch) Then matched = Not matched

If (Not matched) Then Exit Do

iEnd = iEnd + 1


' end of string or GUID should include the closing double
' quote or end of GUID character (close curley brace), so
' increase by one to include these final characters.

' Note: improperly terminated quotes or GUIDs should be impossible
' in "compilable" VBA source code. In the event that the closing
' character is missing, iEnd will already be one past the last
' character of the input line/string, so no need to advance it.

If ((tt = TT_STRING And c = dQuote) Or _
(tt = TT_GUID And c = GUID_END)) Then
iEnd = iEnd + 1
End If

end_of_token = iEnd

End Function


' returns a meaningless, sequential, variable name (that is
' also reasonably short).

Private Function invisible_variable_name(var_id As Long) As String
Dim result As String
Dim i As Long
Dim L1 As Integer
Dim L2 As Integer

assert var_id > 0
result = ""
' this algorithm obtains a valid, short, and meaningless identifier by
' expressing the given integer variable id as a "mixed base"
' number whose "digits" are the characters valid in an identifier.

' Specifically, if you think of the variable id integer as expressed as:

' var_id = i0 + L1* (i1 + L2*i2 + L2^2*i3 + L2^3*i4 + ... )

' (by a slight generalization of the basic ideas of "base X" numbers
' you can show that any positive integer can be expressed in such a
' "mixed L1/L2 base" form)

' where L1 is the length of the valid initial identifier characters
' string; L2 is the length of the valid non-initial identifier
' character string; i0 is an integer index (0..L1-1) into the initial
' identifier char string, and i1, i2, ... are indexes (0...L2-1) into
' the non-initial identifier char string. Then the chars associated
' with these indexes determine the chars in a valid identifier (variable
' name) uniquely determined by var_id.

i = var_id
L1 = Len(firstIdentifierChars)
L2 = Len(identifierChars)
result = Mid(firstIdentifierChars, 1 + i Mod L1, 1)
i = Fix(i / L1)
Do While (i > 0)
result = result & Mid(identifierChars, 1 + i Mod L2, 1)
i = Fix(i / L2)

invisible_variable_name = result

End Function

' Associates an appropriate obfuscated name with each member
' of the invisible names collection
' Also excludes names from the invisible names collection
' that are also on the visible names collection.

Private Sub define_obfuscated_names()
Dim iName As Long
Dim vName As String
Dim cNew As New Collection
Dim iObfuscated_Name As Long

iObfuscated_Name = 1
For iName = 1 To invisible_names.Count
If (lookup_identifier(visible_names, CStr(invisible_names.Item(iName)(NAME_ID))) _
= NO_SUCH_ID) Then
Do ' keep looking until we get a name that is not on either
' the visible or invisible list; this loop executes once, on
' average, because collisions are unlikely Note: Assuring the
' new name isn't on the invisible list is required to avoid
' errors when renaming module, class and userform names.
vName = invisible_variable_name(iObfuscated_Name)
iObfuscated_Name = iObfuscated_Name + 1
Loop Until _
lookup_identifier(visible_names, vName) = NO_SUCH_ID And _
lookup_identifier(invisible_names, vName) = NO_SUCH_ID
add_identifier cNew, CStr(invisible_names.Item(iName)(NAME_ID)), vName
'else identifier is on visible list, so elide it from invisible list
End If
Next iName

Set invisible_names = cNew

End Sub

' returns the (possibly obfuscated, transformed) variable
' name given the original variable name

Private Function var_name(plaintextVarname As String) As String
Dim result As String
result = lookup_identifier(invisible_names, LCase(plaintextVarname))
If (result = NO_SUCH_ID) Then
' just keep the original name except converted to lowercase
result = LCase(plaintextVarname)
End If
var_name = result
End Function

' clears all of the elements in the lookup table
Private Sub reset_lookup_table(lookup_table As Collection)
Set lookup_table = New Collection
End Sub

' returns the value associated with given name, or NO_SUCH_ID if there
' is not such a (name, value) pair in the collection.
Private Function lookup_identifier(c As Collection, sName As String) As String
Dim result As String
On Error GoTo not_found
result = c.Item(LCase(sName))(VALUE_ID)
GoTo end_of_function
result = NO_SUCH_ID
lookup_identifier = result
End Function

Private Sub remove_identifier(c As Collection, sName As String)
If (lookup_identifier(c, sName) <> NO_SUCH_ID) Then
c.Remove LCase(sName)
End If
End Sub

' adds the name, value pair to the collection if the name is
' not already on the collection.

Private Sub add_identifier(c As Collection, sName As String, sValue As String)
Dim name_value_pair(NAME_ID To VALUE_ID) As String

If (NO_SUCH_ID = lookup_identifier(c, sName)) Then
name_value_pair(NAME_ID) = sName
name_value_pair(VALUE_ID) = LCase(sValue)
c.Add name_value_pair, LCase(sName)
End If

End Sub

' location of the last substring within the given string, or 0 if
' substring doesn't occur within given string.

Private Function last_substring_position(s As String, subS As String) As Integer
Dim iFound As Integer
Dim iNext As Integer

iFound = 0
iNext = InStr(1, s, subS)
Do While (iNext > 0)
iFound = iNext
iNext = InStr(iFound + 1, s, subS)

last_substring_position = iFound

End Function

' location of the event delimiter ("_") within the token, or 0 if none.

Private Function event_delimiter_position(token As String) As Integer
event_delimiter_position = last_substring_position(token, userform_event_delimiter)
End Function

' Returns the part of an event procedure token associated with the
' name of an event. For example, with an event procedure token of
' "myButton_Click", returns "Click"

' if the token isn't in the general format of an event procedure name,
' (e.g. it doesn't contain an underscore) it returns NO_SUCH_ID

Private Function event_part(token As String) As String
Dim iPosition As Integer
Dim result As String

iPosition = event_delimiter_position(token)
If (iPosition = 0) Then
result = NO_SUCH_ID
result = Right(token, Len(token) - (iPosition + Len(userform_event_delimiter) - 1))
End If

event_part = result

End Function

' returns the part of an event procedure name associated with the
' name of the object (e.g. myButton_Click as token would return myButton)

Private Function object_part(token As String) As String
Dim iPosition As Integer
Dim result As String

iPosition = event_delimiter_position(token)
If (iPosition = 0) Then
result = NO_SUCH_ID
result = Left(token, iPosition - 1)
End If

object_part = result

End Function

' does the token represent an event procedure name (e.g. myButton_Click) ?
Private Function is_event_procedure(token As String) As Boolean
Dim sEvent As String
Dim result As String
sEvent = event_part(token)
If (sEvent = NO_SUCH_ID) Then
result = False
ElseIf (NO_SUCH_ID = lookup_identifier(userform_attribute_names, sEvent)) Then
result = False
result = True
End If
is_event_procedure = result
End Function

' does the given string begin with the specified prefix?
Private Function has_prefix(s As String, prefix As String) As Boolean
has_prefix = (Left(s, Len(prefix)) = prefix)
End Function

' does the given string end with the specified suffix?
Private Function has_suffix(s As String, suffix As String) As Boolean
has_suffix = (Right(s, Len(suffix)) = suffix)
End Function

' sets token buffer to the default, "do nothing", token sequence
Private Sub reset_token_buffer()
Dim i As Integer
For i = LBound(prevTokens) To UBound(prevTokens)
prevTokens(i) = " " ' use whitespace because leading whitespace cannot change how a program is parsed
Next i ' (the default "" isn't a valid token and can therefore cause problems)
tokenIndex = LBound(prevTokens)
End Sub

' write the token into the circular token buffer
Private Sub remember_token(token As String)
tokenIndex = (tokenIndex + 1) Mod N_BUFFERED_TOKENS
prevTokens(tokenIndex) = token
End Sub

' returns the last token stored in the token buffer
Private Function last_token() As String
last_token = prevTokens(tokenIndex)
End Function

' returns next-to-the-last token stored in the token buffer
Private Function next_to_last_token() As String
Dim result As String
If (tokenIndex = LBound(prevTokens)) Then
result = prevTokens(UBound(prevTokens)) ' wrap-around to last element
result = prevTokens(tokenIndex - 1) ' no-wrap-around needed
End If
next_to_last_token = result
End Function

' does the token represent an attribute (event or property) of a
' control contained on a userform?
Private Function is_userform_attribute(token As String) As Boolean
is_userform_attribute = (NO_SUCH_ID <> lookup_identifier(userform_attribute_names, token))
End Function

' Is the token one that, when it preceeds another token (separated
' only by whitespace) indicates that that token represents an
' explicitly declared name.
' Examples (the variable x is explicitly declared because it is
' preceeded by Dim, Private, or Function):
' dim x as Double
' private x as Variant
' private function x()

Private Function preceeds_declared_name(token As String) As Boolean
Dim result As Boolean

Select Case LCase(token)

Case "friend", "enum", "declare", "static", "byref", "byval", "get", "let", "set", "dim", _
"function", "sub", "type", "const", "private", "public", "global", "paramarray", _
"optional", "property"

result = True
Case Else
result = False
End Select

preceeds_declared_name = result
End Function
' is the token one that, when it follows another token separated only
' by whitespace, implies that token is an explicitly declared name?

' Example (the name x is explicitly declared because it is followed
' by "as"):
' type myType
' x as Integer
' end type

Private Function follows_declared_name(token As String) As Boolean
Dim result As Boolean
Select Case LCase(token)
Case "as", "lib"
result = True
Case Else
result = False
End Select
follows_declared_name = result
End Function

' adds ids contained in the string (representing a single, though possibly
' continued, line of input source text) to appropriate lookup tables
' used to determine which variable names remain unchanged, and which
' are obfuscated (replaced with variable names meaningless to humans).

Private Sub register_ids(s As String)
Dim iStart As Integer
Dim iEnd As Integer
Dim visible As Boolean
Dim obfuscated_id As Long
Dim token As String

reset_token_buffer ' cross-source-statement token sequences are not of interest

If InStr(1, LCase(s), BEGIN_VISIBLE_KEYWORD) <> 0 Then
m_visible_depth = m_visible_depth + 1
End If
If InStr(1, LCase(s), END_VISIBLE_KEYWORD) <> 0 Then
m_visible_depth = m_visible_depth - 1
End If

If InStr(1, LCase(s), VISIBLE_KEYWORD) > 0 Then
' single line #visible keyword makes ids on this line visible, no
' matter what our visible depth is
visible = True
' no line specific keyword, so based on if we are within
' a #begin_visible ... #end_visible bracketed region
visible = m_visible_depth > 0
End If

iStart = 1
Do While (iStart <= Len(s))
iEnd = end_of_token(s, iStart)
token = LCase(Mid(s, iStart, iEnd - iStart))

If (token_type(token, 1) = TT_IDENTIFIER) Then
If (last_token() = userform_control_attribute_flag) Then
' token is flagged as representing a userform-related event,
' such as Click (or control property such as Caption)
' Example token sequence: "_" followed by "Click" will
' register "Click" as a userform attribute. Note that "_Click"
' isn't processed as a single token because "_" isn't a valid
' first character of a variable name in VBA.
add_identifier userform_attribute_names, token, token
add_identifier visible_names, token, token
ElseIf (is_event_procedure(token)) Then
' example token: myButton_Click will make itself and myButton visible if _Click is listed in visible_names.txt
add_identifier visible_names, token, token
add_identifier visible_names, object_part(token), object_part(token)
ElseIf (is_userform_attribute(token) And _
last_token() = object_attribute_delimiter And token_type(next_to_last_token(), 1) = TT_IDENTIFIER) Then
' example: myLabel.Caption will make myLabel a visible name if
' "_Caption" is listed in visible_names.txt (the leading _
' flags Caption as a userform control attribute (event or property))
add_identifier visible_names, next_to_last_token(), next_to_last_token()
ElseIf (visible) Then
add_identifier visible_names, token, token
' note: if an identifier gets added to both visible and
' invisible lists, it will considered visible (and get removed
' from the invisible list in a separate step later on).
If (token_type(last_token(), 1) = TT_WHITESPACE) Then
If (preceeds_declared_name(next_to_last_token())) Then
add_identifier invisible_names, token, token
End If

If (follows_declared_name(token) And _
token_type(next_to_last_token(), 1) = TT_IDENTIFIER) Then
add_identifier invisible_names, next_to_last_token(), next_to_last_token()
End If
End If
End If
' else not an identifier, so it can never be added to lookup tables
' used to determine token visibility.
End If

remember_token token ' stores last few token in a circular buffer for easier parsing

iStart = iEnd

End Sub

' the length of a string, excluding and leading/trailing double quotes
Private Function length_sans_quotes(s As String) As Integer
Dim result As Integer
result = Len(s)
If (has_prefix(s, dQuote)) Then result = result - Len(dQuote)
If (has_suffix(s, dQuote)) Then result = result - Len(dQuote)
length_sans_quotes = result
End Function

' length of the given prefix within a specified string, or 0 if that
' prefix is not at the beginning of the specified string
Private Function length_of_prefix(s As String, prefix As String) As Integer
Dim result As Integer
If (has_prefix(s, prefix)) Then
result = Len(prefix)
result = 0
End If
length_of_prefix = result
End Function
' strips leading, trailing, double quotes from a given string
' (if no such quotes present, returns original string)
Private Function NQ(s As String) As String
NQ = Mid(s, 1 + length_of_prefix(s, dQuote), length_sans_quotes(s))
End Function

' adds double quotes around the given string
Private Function Q(s As String) As String
Q = dQuote & s & dQuote
End Function

' returns an obfuscated, functionally equivalent, source code line
' for the given source code line
Private Function obfuscated_line(s As String) As String
Dim result As String
Dim iStart As Integer
Dim iEnd As Integer
Dim token As String

result = ""

iStart = 1

Do While (iStart <= Len(s)) iEnd = end_of_token(s, iStart) token = Mid(s, iStart, iEnd - iStart) Select Case (token_type(token, 1)) Case TT_IDENTIFIER result = result & var_name(token) Case TT_WHITESPACE If (InStr(token, line_continuation_chars & vbNewLine) > 0) Then
' line continuation characters and newlines are analyzed as
' part of whitespace tokens, but they need to be preserved
' because VBA has line length constraints that could break code
' if long continued lines were collapsed into a single line.
result = result & line_continuation_chars & vbNewLine
result = result & " "
End If
result = result & token
result = result & token
If (has_prefix(token, doubleComment)) Then
' double comments are retained (for copywrite notices, etc.)
result = result & Right(token, Len(token) - Len(firstCommentChar))
' else just ignore/elide the comment
End If
result = result & token
result = result & token
Case Else
assert False ' should have been type "other"
End Select
iStart = iEnd

' trim to drop any leading whitespace (makes lines all flush left)
obfuscated_line = Trim(result)

End Function

' reads each line from the specified sourcecode file, and
' registers any identifiers contained in the file on the
' appropriate (visible or invisible) lookup table.

Private Sub register_identifiers(fName As String)
Dim fid As Integer
Dim sLine As String
Dim errNo As Long
On Error GoTo error_exit

' open file for reading
fid = freefile()
Open fName For Input As #fid

' read each (possibly continued) line, registering its ids
Do While Not EOF(fid)
sLine = get_continued_line(fid)
register_ids sLine

Close fid
GoTo end_of_sub
errNo = Err.Number
On Error Resume Next
Close fid
Err.Raise errNo

End Sub

' is the line one that is continued on the next line (ends in
' the VBA line continuation character sequence, " _")
Private Function is_continued_line(sLine As String) As Boolean
is_continued_line = has_suffix(sLine, line_continuation_chars)
End Function


' adds another line to an existing series of "vbNewLine
' separated" lines, returning the so-extended series of lines.

Private Function add_line(sOld As String, sNew As String) As String
Dim result As String
If (sOld = "") Then
result = sNew
result = sOld & vbNewLine & sNew
End If
add_line = result
End Function

' returns a (possibly continued) source code line from the given
' input file.
Private Function get_continued_line(f_in As Integer) As String
Dim result As String
Dim sTmp As String
result = ""
Do ' read & concatenate continued lines
Line Input #f_in, sTmp
result = add_line(result, sTmp)
Loop Until EOF(f_in) Or Not is_continued_line(sTmp)

get_continued_line = result

End Function


' obfuscates the given sourcecode file by removing comments,
' replacing meaningful names with meaningless names, etc.

' A side benefit: it tends to reduce the size of the source code
' files, due to comment elimination and the fact that
' obfuscated names are usually substantially shorter than
' the original names.

Private Sub obfuscate_sourcecode_file( _
f_plain As String, f_obfuscated As String)
Dim f_in As Integer
Dim f_out As Integer
Dim sLine As String
Dim sObfuscated As String
Dim errNo As Long
On Error GoTo error_exit

f_in = freefile()
Open f_plain For Input As #f_in
f_out = freefile()
Open f_obfuscated For Output As #f_out

' obfuscate, and then write, each original input source code
' file line into the obfuscated source code output file
Do While Not EOF(f_in)
sLine = get_continued_line(f_in)
sObfuscated = obfuscated_line(sLine)
If (m_interleave_original_code_as_comments) Then
Print #f_out, firstCommentChar & sLine
Print #f_out, sObfuscated ' empty obfuscated lines retained--helpful when debugging.
ElseIf (sObfuscated <> "") Then
Print #f_out, sObfuscated
' else elide lines that are empty after obfuscation
End If


Close f_in
Close f_out
GoTo end_of_sub
errNo = Err.Number
On Error Resume Next
Close f_in
On Error Resume Next
Close f_out
Err.Raise errNo


End Sub

' returns a temporary file name given a file number
Private Function temp_file_name(wb As Workbook, _
iFile As Integer, Optional extension = ".tmp") As String
temp_file_name = wb.Path & PS() & IB_TEMP_FILENAME_PREFIX & _
CStr(iFile) & extension
End Function

' returns a random module name suitable for use as a VBA code module
Private Function random_module_name() As String
' highly unlike this name will conflict with any existing names
random_module_name = "qzx" & _
Format(10 ^ 6 * Rnd(), "000000") & Format(10 ^ 6 * Rnd(), "000000")
End Function

' writes source code in a given VBComponent into a specified file
' (overwrites any existing file contents)

Private Sub write_component_code(vbc As Object, f As String)
Dim f_out As Integer
Dim iLine As Long
Dim errNo As Long
On Error GoTo error_exit

f_out = freefile()
Open f For Output As #f_out

For iLine = 1 To vbc.CodeModule.CountOfLines
Print #f_out, vbc.CodeModule.Lines(startLine:=iLine, Count:=1)
Next iLine

Close f_out
GoTo end_of_sub
errNo = Err.Number
On Error Resume Next
Close f_out
Err.Raise errNo


End Sub

' reads source code in a given file into the specified component
' (overwrites any existing code in the component)

Private Sub read_component_code(vbc As Object, f As String)
Dim f_in As Integer
Dim sLine As String
Dim iLine As Long
Dim errNo As Long
On Error GoTo error_exit

vbc.CodeModule.DeleteLines startLine:=1, Count:=vbc.CodeModule.CountOfLines

' vbc.CodeModule.AddFromFile has unpleasant side-effects related to module name
' changes, so we just add the lines one at a time instead:
f_in = freefile()
Open f For Input As #f_in
iLine = 1
Do While Not EOF(f_in) ' read each source code line and insert into component
Line Input #f_in, sLine
vbc.CodeModule.InsertLines iLine, sLine
iLine = iLine + 1

Close f_in
GoTo end_of_sub
errNo = Err.Number
On Error Resume Next
Close f_in
Err.Raise errNo


End Sub

' Writes out a "cheat sheet" that gives you the original name of each
' obfuscated name in an obfuscated workbook.
' The cheat sheet is helpful in debugging obfuscated programs (allows
' you to translate the names that appear on a single obfuscated line,
' etc.).
' Assumes that invisible_names list is fully populated.

Private Sub write_invisible_names(wb As Workbook, fName As String)
Dim iPair As Long ' index of name, value pair on invisible names list
Dim f_out As Integer
Dim errNo As Long
On Error GoTo error_exit

f_out = freefile()
Open fName For Output As #f_out

Print #f_out, "Hidden" & vbTab & "Original"
For iPair = 1 To invisible_names.Count ' for each name, value pair on invisible names list
Print #f_out, invisible_names(iPair)(VALUE_ID) & vbTab & invisible_names(iPair)(NAME_ID)
Next iPair

Close f_out
GoTo end_of_sub
errNo = Err.Number
On Error Resume Next
Close f_out
Err.Raise errNo


End Sub

' obfuscates all VBA source code modules, classes and UserForms

Private Sub obfuscate_workbook(wb As Workbook)
Dim vbc As Object
Dim iFile As Integer
Dim tmpFile As String
Dim old_display_status_bar As Boolean
Dim newName As String

old_display_status_bar = Application.DisplayStatusBar
Application.DisplayStatusBar = True

Application.StatusBar = "Saving Invisibly: initializing..."
' start with empty variable name identifier tables
reset_lookup_table visible_names
reset_lookup_table invisible_names
reset_lookup_table userform_attribute_names
' the E "identifier" appears within numeric literals
' expressed in scientific notation, and thus must never be
' obfuscated (this "non-obfuscation of e" is needed because
' our lexical analysis of numbers is otherwise too simple to
' get numeric literals expressed in scientific notation right).
register_ids "e '#visible"

' register all built-in visible identifiers stored in
' a special text file shipped with the application
' (Excel/VBA keywords and user defined universal keywords)

assert Dir(ThisWorkbook.Path & PS() & IB_VISIBLE_KEYWORDS_FILENAME) <> ""
m_visible_depth = 1
register_identifiers ThisWorkbook.Path & PS() & IB_VISIBLE_KEYWORDS_FILENAME
m_visible_depth = 0

' first pass: store each code module in a temp file,
' register that file's visible identifiers, and then delete
' the code component.

For iFile = 1 To wb.VBProject.VBComponents.Count
Set vbc = wb.VBProject.VBComponents(iFile)
Select Case vbc.Type
Case IB_vbext_ct_StdModule, IB_vbext_ct_ClassModule, IB_vbext_ct_MSForm
' the name of a module, class, or userform is obfuscated
' (normal case). Register the name as "invisible"
m_visible_depth = 0
register_ids "Dim " & vbc.Name ' Dim makes it look like name is "user declared"
Case IB_vbext_ct_Document
' document (e.g. Worksheet) code names remain visible because
' there isn't an easy way to RELIABLY change them
' programmatically (surprisingly, setting vbc.Name doesn't do it)
register_ids vbc.Name & " '#visible"
Case Else
' if Microsoft adds a new type, play it safe by keeping
' names unchanged ("visible").
register_ids vbc.Name & " '#visible"
End Select
Application.StatusBar = "Saving Invisibly: Pass 1 of 2, VBComponent " & CStr(iFile) & " of " & CStr(wb.VBProject.VBComponents.Count)
write_component_code vbc, temp_file_name(wb, iFile)
m_visible_depth = 0 ' invisible unless otherwise noted
register_identifiers temp_file_name(wb, iFile)
Next iFile

define_obfuscated_names 'choose obscure ids for invisible names

' second pass obfuscates by replacing registered, non-visible
' variable ids with meaningless ids, stripping comments, etc, and
' then reading the so-obfuscated code back into each component.

tmpFile = temp_file_name(wb, wb.VBProject.VBComponents.Count + 1)
For iFile = 1 To wb.VBProject.VBComponents.Count
Set vbc = wb.VBProject.VBComponents(iFile)

newName = obfuscated_line(vbc.Name)
' this "if" (to prevent changing name when name isn't obfuscated)
' was added because I don't trust that name changes in such cases, even to the same
' name, are reliable.
If (LCase(newName) <> LCase(vbc.Name)) Then vbc.Name = newName

Application.StatusBar = "Saving Invisibly: Pass 2 of 2, VBComponent " & CStr(iFile) & " of " & CStr(wb.VBProject.VBComponents.Count)
obfuscate_sourcecode_file temp_file_name(wb, iFile), tmpFile
read_component_code vbc, tmpFile
Kill tmpFile
Kill temp_file_name(wb, iFile)
Next iFile

Application.StatusBar = _
"Writing ""secret decoder"" file: " & ib_suffixed_filename(wb, IB_SECRET_DECODER_SUFFIX) & "..."
write_invisible_names wb, ib_suffixed_filename(wb, IB_SECRET_DECODER_SUFFIX)

Application.StatusBar = False ' restore status bar status quo
Application.DisplayStatusBar = old_display_status_bar

End Sub

' If this function returns True, the two strings are guaranteed
' to represent different physical files (regardless of what
' default paths might be added to any file name strings that do not
' have explicitly specified full pathnames)

Private Function are_different_files(f1_in As String, f2_in As String) As Boolean
Dim f1 As String
Dim f2 As String
Dim result As Boolean

f1 = Trim(LCase(f1_in))
f2 = Trim(LCase(f2_in))

' making each filename start with path separator simplifies the tests:
If (Not has_prefix(f1, PS())) Then f1 = PS() & f1
If (Not has_prefix(f2, PS())) Then f2 = PS() & f2

' if the last half of either filename string equals the other,
' the filename COULD represent the same physical file
If (has_suffix(f1, f2) Or has_suffix(f2, f1)) Then
result = False
' filenames definitely represent different files
result = True
End If
are_different_files = result
End Function

' obfuscates the given workbook, saving it into the specified file

Public Sub obfuscate_workbook_as(wb As Workbook, fileName As String)

assert are_different_files(wb.fullName, fileName)

' saving under a new name breaks connection with original file,
' assuring that original unobfuscated workbook isn't damaged.
' (even if we crash and user then accidentally saves the
' so-damaged workbook, originally named file is still safe)
wb.SaveAs fileName
obfuscate_workbook wb
Application.DisplayAlerts = False
wb.SaveAs fileName ' save again under the new name
Application.DisplayAlerts = True

End Sub

' default filename in which to store "invisible" version
Private Function ib_suffixed_filename(wb As Workbook, suffix As String) As String
Dim dot_position As Integer
Dim result As String
dot_position = last_substring_position(wb.Name, ".")
If (dot_position = 0) Then
result = wb.Path & PS() & wb.Name & suffix
result = wb.Path & PS() & Left(wb.Name, dot_position - 1) & suffix
End If
ib_suffixed_filename = result
End Function

' Save the active workbook invisibly in a user-selected workbook
Private Sub ib_save_invisibly_as()
Dim fileName As String
Dim wb As Workbook
On Error GoTo error_exit

Set wb = ActiveWorkbook
If (Not wb.saved) Then
MsgBox "Workbook """ & ActiveWorkbook.Name & """ has unsaved changes. " & _
"To help prevent accidental source code losses, workbooks " & _
"with unsaved changes cannot be saved invisibly. " & vbNewLine & vbNewLine & _
"Save your original workbook, then try again. ", _
vbCritical, "Workbooks with unsaved changes cannot be saved invisibly."
GoTo end_of_sub
End If
' present a "save as" type filename dialog
fileName = Application.GetSaveAsFilename( _
InitialFilename:=ib_suffixed_filename(wb, IB_FILENAME_SUFFIX), _
FileFilter:="Microsoft Excel Workbook (*.xls),*.xls,All Files (*.*),*.*", _
Title:="Select file into which workbook will be saved invisibly")

' Because there is too much potential for total code loss, we do not
' allow user to overwrite the original workbook with the obfuscated
' workbook:

If (Not are_different_files(wb.fullName, fileName)) Then
MsgBox "The selected filename (" & fileName & _
") must be clearly different from the current workbook's filename (" & wb.fullName & _
"). Try again, next time choosing a different name.", _
vbCritical, "Save Invisibly As Filename Must Differ from Original Filename"
ElseIf (fileName <> "False") Then
obfuscate_workbook_as wb, fileName
End If

GoTo end_of_sub
Application.DisplayAlerts = True
Application.StatusBar = False ' resume default status bar behavior
MsgBox "Error #" & CStr(Err.Number) & " during ""Save Invisibly As"": " & Err.Description, _
vbCritical, "Invisible Basic Save Invisibly As Error"
End Sub

' Top level "Save Invisibly As..." command

Public Sub invisible_basic_save_invisibly_as() '#visible
m_interleave_original_code_as_comments = False
End Sub

' Top level "Debugging Save Invisibly As..." command

Public Sub invisible_basic_debugging_save_invisibly_as() '#visible
m_interleave_original_code_as_comments = True
End Sub

Private Sub visit_url(url As String)
ShellExecute GetDesktopWindow(), "Open", url, 0, 0, SW_SHOWMAXIMIZED
End Sub

' Just shows the HTML file that contains the InvisibleBasic help file
Public Sub invisible_basic_show_help() '#visible
visit_url ThisWorkbook.Path & PS() & "InvisibleBasic.html"
End Sub

Public Sub invisible_basic_web_site() '#visible
End Sub

Public Sub invisible_basic_about() '#visible

MsgBox "Invisible Basic Version " & CStr(INVISIBLE_BASIC_VERSION) & vbNewLine & _
"A Source Code Obfuscator for Excel/VBA" & vbNewLine & _
"Share you spreadsheets. Not your source code." & vbNewLine & _
vbNewLine & _
"Copyright 2006, John C. Gunther. All Rights Reserved." & vbNewLine & _
"Distributed under the terms of the BSD open source license." & vbNewLine & _
vbNewLine & _
"Web Site: " & INVISIBLE_BASIC_URL & vbNewLine _
, vbOKOnly, "About Invisible Basic"

End Sub

' adds or updates the Invisible Basic menu within Excel
Public Sub invisible_basic_add_menu()
Dim cbp As CommandBarPopup ' new invisible basic menu bar
Dim cbb As CommandBarButton ' new menu item added to this bar

Call invisible_basic_remove_menu ' to prevent adding menu twice

Set cbp = Application.CommandBars(IB_NameOfExcelWorksheetMenubar).Controls.Add( _

cbp.caption = IB_NameOfInvisibleBasicMenu
cbp.tooltiptext = _
"Source code obfuscation utility for Excel/VBA applications."

Set cbb = cbp.Controls.Add(Type:=msoControlButton)
cbb.caption = "&Save Invisibly As..."
cbb.DescriptionText = "Saves copy of workbook whose VBA code is replaced with equivalant, but hard-to-read, code."
cbb.onAction = "invisible_basic_save_invisibly_as"

Set cbb = cbp.Controls.Add(Type:=msoControlButton)
cbb.caption = "&Debugging Save Invisibly As..."
cbb.DescriptionText = "Same as Save Invisibly As except interleaves original source code as comments (for debugging)."
cbb.onAction = "invisible_basic_debugging_save_invisibly_as"

Set cbb = cbp.Controls.Add(Type:=msoControlButton)
cbb.caption = "&Help..."
cbb.DescriptionText = "Invisible Basic Help"
cbb.onAction = "invisible_basic_show_help"

Set cbb = cbp.Controls.Add(Type:=msoControlButton)
cbb.caption = "Invisible Basic &Web Site"
cbb.DescriptionText = "Invisible Basic Web Site"
cbb.onAction = "invisible_basic_web_site"

Set cbb = cbp.Controls.Add(Type:=msoControlButton)
cbb.caption = "&About Invisible Basic..."
cbb.DescriptionText = "About Invisible Basic"
cbb.onAction = "invisible_basic_about"

End Sub

' removes the Invisible Basic menu from Excel
Public Sub invisible_basic_remove_menu()
On Error Resume Next
Application.CommandBars(IB_NameOfExcelWorksheetMenubar).Controls( _
End Sub

' Simple test of Invisible Basic. Test requires that the test
' workbook, IB_Test.xls, be in the same folder as the Add-in is.
' The test makes the test workbook invisible, then runs a test
' routine within the (then obfuscated) test workbook.
' You may see two "OK to overwrite" prompts (answer Yes)
' and you should see "Hello Invisible Basic" (four times)
' if the test passes. If you don't see "Hello Visible Basic",
' four times, the test has failed.
Public Sub ib_test()
Dim wb As Workbook
Dim fTest As String
Dim fObf As String
Dim iPass As Integer

assert event_part("myButton_Click") = "Click"
assert object_part("myButton_Click") = "myButton"
assert event_part("myButtonClick") = ""
assert event_part("myButton_20_Click") = "Click"
assert object_part("myButton_20_Click") = "myButton_20"

For iPass = 1 To 2
If (iPass = 1) Then
invisiblebasic.interleave_original_code_as_comments = False
invisiblebasic.interleave_original_code_as_comments = True
End If

fTest = ThisWorkbook.Path & PS() & "IB_Test.xls"
fObf = ThisWorkbook.Path & PS() & "IB_Test_Obf.xls"
' Open the test workbook
Workbooks.Open fTest
Set wb = Workbooks(Workbooks.Count)

' Save it invisibly as a new workbook
obfuscate_workbook_as wb, fObf

' the test module exercies code in the obfuscated modules in IB_Test
' and compares results with expected results.
Evaluate "IBTest.testModule.ibt_test()"
wb.close SaveChanges:=False
Next iPass
End Sub

Leave a Reply

Your email address will not be published. Required fields are marked *