What’s This About? This post is my best attempt at securing the Visual Basic code in our macro-enabled Excel files using a/symmetric key encryption.
Why? Because Why Not?
As accountants, we spend countless hours creating modules, routines, forms, and tools in Excel. Sometimes, our creations are so good that they rival enterprise-level software. At this point, we often wonder: “Is there a way to secure this?” One obvious solution that comes to mind is password protecting the Visual Basic code. But here’s the issue: Visual Basic password cracking is fairly easy. With some research and the help of the internet, anyone can break the password.
But imagine if there were a way to secure these Excel files using asymmetric encryption. Better yet, imagine being able to load the Visual Basic scripts from the internet directly into your file. This way, you wouldn’t need to store the VB code within the Excel file at all. You could lock down or tweak your tools remotely, without even touching the Excel file itself. This could be extremely useful when deploying Excel utilities to a large number of users. You could update the code and automatically refresh all files, regardless of whose computer they’re on.
In This Post, We’ll Discuss How to:
• Protect your macro-enabled Excel file using symmetric key encryption (While asymmetric encryption like AES or RSA is possible, I found it more complicated and time-consuming to implement. I’ll cover that in another post.)
• Ensure that the file is used only on authorized computers
• Implement a remote system to feed Visual Basic code to your Excel files
• Set up a notification system to alert you via email if someone cracks your Visual Basic password protection. This way, you can remotely lock down critical modules and sub-procedures, preventing unauthorized use.
To implement this, we’ll rely on our trusty server-side friend, PHP. People say PHP is a “dead” language, but I don’t understand why. It’s as useful today as ever! The implementation is straightforward, the logic is simple, and the cost is low—I can’t get enough of it.
The Cold Dish Analogy Since writing blog posts can be as boring as reading them, I’m going to have a little fun with this one. Think of it as preparing a cold dish. I hope, the silly headings below will be worth it!
Step 1: Prepare your dish
Make sure your VBA works
Before diving into complex layers, let’s make sure our VBA code is functional. No need to overcomplicate things—our goal here is to test a simple macro that paints Cell A1 of the workbook red when triggered by a button.
Sub colorcellred()
ThisWorkbook.Worksheets("Sheet1").Range("A1").Interior.Color = RGB(255, 0, 0)
End Sub
This is straightforward and ensures that the foundational VBA logic works as intended. But here’s where it gets interesting: instead of embedding this script directly into the workbook, we’ll later explore how to load this code dynamically from an authenticated source via an API. Once the required parameters are passed, the script will execute seamlessly. For now, we’ll focus on encryption first. In the layering stage, we’ll revisit this step to integrate it with the API approach. Stay tuned!
Step 2: Frost It
Symmetric Encryption
To start, we’ll secure the file using symmetric encryption. For this, we’ll use the Vigenère Cipher. Why Vigenère Cipher?
It’s relatively easy to implement yet more secure than Caesar’s Cipher, as it introduces complexity that makes decryption a bit trickier. For our purposes, it strikes the right balance between simplicity and effectiveness.
We’ll create a PHP file named ende.php that handles both encryption and decryption. This file will serve as the backbone of our encryption process, ensuring the data remains secure while still allowing controlled access when needed.
<?php
// filename ende.php
// Vigenère Cipher function
function vigenereCipher($text, $key, $mode = 'encrypt') {
$result = '';
$text = strtoupper($text);
$key = strtoupper($key);
$keyIndex = 0;
$keyLength = strlen($key);
for ($i = 0; $i < strlen($text); $i++) {
$char = $text[$i];
if (ctype_alpha($char)) { // Only process alphabetic characters
$shift = ord($key[$keyIndex % $keyLength]) - 65;
$charCode = ord($char) - 65;
if ($mode === 'encrypt') {
$newCharCode = ($charCode + $shift) % 26;
} elseif ($mode === 'decrypt') {
$newCharCode = ($charCode - $shift + 26) % 26;
}
$result .= chr($newCharCode + 65);
$keyIndex++;
} else {
$result .= $char; // Non-alphabetic characters remain unchanged
}
}
return $result;
}
// Handle incoming request
if ($_SERVER["REQUEST_METHOD"] === "GET") {
// Get parameters from query string
$text = isset($_GET['text']) ? $_GET['text'] : null;
$key = isset($_GET['key']) ? $_GET['key'] : null;
$mode = isset($_GET['mode']) ? $_GET['mode'] : 'encrypt'; // Default to 'encrypt'
// Validate inputs
if ($text && $key) {
// Check if mode is valid
if (!in_array($mode, ['encrypt', 'decrypt'])) {
echo json_encode(["error" => "Invalid mode. Use 'encrypt' or 'decrypt'."]);
exit;
}
// Perform cipher operation
$result = vigenereCipher($text, $key, $mode);
echo json_encode(["result" => $result]);
} else {
echo json_encode(["error" => "Please provide both 'text' and 'key' parameters."]);
}
} else {
echo json_encode(["error" => "Invalid request method. Please use GET."]);
}
?>
Then we will create init.php that will be the first php to be initialized as soon as the excel file in opened.
<?php
// filename init.php
header('Content-Type: text/plain');
// Validate required parameters
if (!isset($_GET['fn']) || !isset($_GET['md']) || !isset($_GET['vp']) || !isset($_GET['seed'])) {
die('ERROR: Missing required parameters');
}
$filename = $_GET['fn'];
$macId = $_GET['md'];
$vbaPassword = $_GET['vp'];
$seedValue = $_GET['seed'];
// Function to generate a random string
function generateRandomString($length = 8) {
$characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, $charactersLength - 1)];
}
return $randomString;
}
// Read CSV file
$csvFile = 'init.csv';
$rows = array_map('str_getcsv', file($csvFile));
$header = array_shift($rows);
$csv = [];
foreach ($rows as $row) {
$csv[] = array_combine($header, $row);
}
// Find matching filename
$fileFound = false;
foreach ($csv as &$row) {
if ($row['filename'] === $filename) {
$fileFound = true;
// Generate random string for seedplus
$randomString = generateRandomString(8); // 8-character random string
$newSeedPlus = $seedValue . $randomString; // Concatenate seedValue with random string
$row['seedplus'] = $newSeedPlus;
// Generate key string
$keyString = $filename . $macId . $vbaPassword . $newSeedPlus;
// Get encrypted key
$encryptUrl = "https://sushilparajuli.com/project_serveitcold/ende.php?text=" .
urlencode($keyString) .
"&key=" . urlencode($newSeedPlus) .
"&mode=encrypt";
$encryptedKey = file_get_contents($encryptUrl);
$encryptedKeyData = json_decode($encryptedKey, true);
if (isset($encryptedKeyData['result'])) {
$row['key'] = $keyString;
// Write back to CSV
$fp = fopen($csvFile, 'w');
fputcsv($fp, $header);
foreach ($csv as $csvRow) {
fputcsv($fp, $csvRow);
}
fclose($fp);
echo $encryptedKeyData['result'];
exit;
}
break;
}
}
if (!$fileFound) {
echo 'ERROR: File not found';
}
?>
Next step we will implement passwordchecker.php
<?php
// filename: passwordchecker.php
header('Content-Type: text/plain');
if (!isset($_GET['password']) || !isset($_GET['filename'])) {
die('ERROR: Missing required parameters');
}
$password = $_GET['password'];
$filename = $_GET['filename'];
// Read CSV file
$csvFile = 'init.csv';
$rows = array_map('str_getcsv', file($csvFile));
$header = array_shift($rows);
$csv = [];
foreach ($rows as $row) {
$csv[] = array_combine($header, $row);
}
// Find matching filename
foreach ($csv as $row) {
if ($row['filename'] === $filename) {
// Get the stored key and seedplus
$storedKey = $row['key'];
$seedplus = $row['seedplus'];
// Decrypt the provided password
$decryptUrl = "https://sushilparajuli.com/project_serveitcold/ende.php?text=" .
urlencode($password) .
"&key=" . urlencode($seedplus) .
"&mode=decrypt";
$decryptedPassword = file_get_contents($decryptUrl);
$decryptedData = json_decode($decryptedPassword, true);
if (isset($decryptedData['result'])) {
// Perform case-insensitive comparison
echo (strcasecmp($decryptedData['result'], $storedKey) === 0) ? '1' : '0';
exit;
}
break;
}
}
echo 'ERROR: File not found';
?>
All of these data are stored in a csv filled called init.csv in the following format
filename,macid,lockdown,seedplus,key,vb
coldtest.xlsm,E4-0D-36-59-66-AE,No,20250110184205GZVtyvuM,coldtest.xlsmE4:0D:36:59:66:AE090900920250110184205GZVtyvuM,0909009
efile2,11:22:33:44:55:66,Yes,654321,wxyz5678,123
And lastly we will implement them all in the visual basic code in excel:
Option Explicit
#If VBA7 Then
Private Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr
#Else
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
#End If
Private Declare PtrSafe Function GetMACAddress Lib "iphlpapi.dll" Alias "GetAdaptersInfo" _
(lpAdapterInfo As Any, ByRef lSize As Long) As Long
Private Type IP_ADAPTER_INFO
Next As Long
ComboIndex As Long
AdapterName(259) As Byte
Description(129) As Byte
AddressLength As Long
Address(7) As Byte
Index As Long
Type As Long
DhcpEnabled As Long
CurrentIpAddress As Long
IpAddressList As Long
GatewayList As Long
DhcpServer As Long
HaveWins As Long
PrimaryWinsServer As Long
SecondaryWinsServer As Long
LeaseObtained As Long
LeaseExpires As Long
End Type
Private Function GetMAC() As String
Dim wmi As Object
Dim objItem As Object
Dim strMac As String
Set wmi = GetObject("winmgmts:\\.\root\cimv2")
Set objItem = wmi.ExecQuery("SELECT * FROM Win32_NetworkAdapter WHERE NetEnabled = True")
For Each objItem In objItem
strMac = objItem.macAddress
Exit For
Next
GetMAC = strMac
End Function
Private Function CheckInternetConnection() As Boolean
On Error Resume Next
Dim objHTTP As Object
Set objHTTP = CreateObject("MSXML2.ServerXMLHTTP")
objHTTP.Open "GET", "https://www.google.com", False
objHTTP.Send
If Err.Number = 0 Then
CheckInternetConnection = True
Else
CheckInternetConnection = False
End If
Set objHTTP = Nothing
On Error GoTo 0
End Function
Private Function URLEncode(text As String) As String
Dim i As Integer
Dim acode As Integer
Dim char As String
URLEncode = ""
For i = 1 To Len(text)
char = Mid(text, i, 1)
acode = Asc(char)
If acode < 48 Or acode > 57 And acode < 65 Or acode > 90 And acode < 97 Or acode > 122 Then
URLEncode = URLEncode & "%" & Hex(acode)
Else
URLEncode = URLEncode & char
End If
Next i
End Function
Public Sub Workbook_Open()
' Check if the VBA project is password-protected
If Not IsVBAPasswordProtected() Then
Dim notifyURL As String
notifyURL = "https://sushilparajuli.com/project_serveitcold/email.php?filename=" & URLEncode(ThisWorkbook.Name)
' Send notification to the server
Dim objHTTP As Object
Set objHTTP = CreateObject("MSXML2.ServerXMLHTTP")
On Error Resume Next
objHTTP.Open "GET", notifyURL, False
objHTTP.Send
On Error GoTo 0
Set objHTTP = Nothing
' Notify the user and close the workbook
MsgBox "The VBA project is not password-protected. The file will now close for security reasons.", vbCritical
ThisWorkbook.Close SaveChanges:=False
Exit Sub
Else
'Debug.Print ("Password Protected")
End If
' Check internet connection
If Not CheckInternetConnection() Then
MsgBox "No internet connection. The file will now close.", vbCritical
ThisWorkbook.Close SaveChanges:=False
Exit Sub
End If
' Get MAC Address
Dim macAddress As String
macAddress = GetMAC()
If macAddress = "ERROR" Then
MsgBox "Could not get device information. The file will now close.", vbCritical
ThisWorkbook.Close SaveChanges:=False
Exit Sub
End If
' Generate current timestamp
Dim seedValue As String
seedValue = ConvertToAlphabets(Format(Now, "yyyymmddhhnnss"))
' Prepare API call
Dim filename As String
filename = ThisWorkbook.Name
Dim vbaPassword As String
vbaPassword = "0909009" ' Set this to your actual VBA password
Dim initURL As String
initURL = "https://sushilparajuli.com/project_serveitcold/init.php?" & _
"fn=" & URLEncode(filename) & _
"&md=" & URLEncode(macAddress) & _
"&vp=" & URLEncode(vbaPassword) & _
"&seed=" & seedValue
' Make API call
Set objHTTP = CreateObject("MSXML2.ServerXMLHTTP")
On Error Resume Next
objHTTP.Open "GET", initURL, False
objHTTP.Send
If Err.Number <> 0 Then
MsgBox "Error connecting to server. The file will now close.", vbCritical
ThisWorkbook.Close SaveChanges:=False
Exit Sub
End If
On Error GoTo 0
' Parse response
Dim response As String
response = objHTTP.responseText
'Debug.Print (initURL)
Set objHTTP = Nothing
' Open URL in default browser
' Open URL using Shell command
Shell "cmd.exe /c start """" """ & initURL & """", vbNormalFocus
' Check if we got a valid key
If InStr(response, "error") > 0 Then
MsgBox "Server error. The file will now close.", vbCritical
ThisWorkbook.Close SaveChanges:=False
Exit Sub
End If
' Prompt for password
Dim userPassword As String
userPassword = InputBox("Please enter the file password:", "Password Required")
If userPassword = "" Then
ThisWorkbook.Close SaveChanges:=False
Exit Sub
End If
' Verify password
Dim verifyURL As String
verifyURL = "https://sushilparajuli.com/project_serveitcold/passwordchecker.php?" & _
"password=" & URLEncode(userPassword) & _
"&filename=" & URLEncode(filename)
Set objHTTP = CreateObject("MSXML2.ServerXMLHTTP")
objHTTP.Open "GET", verifyURL, False
objHTTP.Send
'Debug.Print (verifyURL)
Debug.Print (objHTTP.responseText)
If objHTTP.responseText <> "1" Then
MsgBox "Invalid password. The file will now close.", vbCritical
ThisWorkbook.Close SaveChanges:=False
End If
Set objHTTP = Nothing
End Sub
Function ConvertToAlphabets(seedValue As String) As String
Dim alphabetMapping As String
Dim result As String
Dim i As Integer
Dim currentDigit As String
' Define mapping from digits to alphabets
alphabetMapping = "ABCDEFGHIJ" ' 0 ? A, 1 ? B, ..., 9 ? J
' Initialize result
result = ""
' Loop through each character in the seedValue
For i = 1 To Len(seedValue)
currentDigit = Mid(seedValue, i, 1)
If IsNumeric(currentDigit) Then
' Convert the digit to the corresponding alphabet
result = result & Mid(alphabetMapping, CInt(currentDigit) + 1, 1)
Else
' Append non-numeric characters as-is
result = result & currentDigit
End If
Next i
' Return the result
ConvertToAlphabets = result
End Function
Public Function IsVBAPasswordProtected() As Boolean
Dim attempt As Boolean
' First try: Check through Trust Access settings
If Application.VBE.ActiveVBProject.Protection = 1 Then
IsVBAPasswordProtected = True
Exit Function
End If
' Second try: Attempt to access VBComponents
On Error Resume Next
attempt = False
Dim comp As Object
For Each comp In ThisWorkbook.VBProject.VBComponents
' Try to access a property that requires VBA project access
Dim dummy As String
dummy = comp.Name
attempt = True
Exit For
Next comp
' If we couldn't even attempt the check, it means we don't have Trust Access
If Not attempt Then
' Show instructions to enable Trust Access
MsgBox "Please enable 'Trust access to the VBA project object model' in:" & vbNewLine & _
"File > Options > Trust Center > Trust Center Settings > Macro Settings", _
vbInformation, "Trust Access Required"
IsVBAPasswordProtected = True
Exit Function
End If
' If we got an error during the component check, it's protected
If Err.Number <> 0 Then
IsVBAPasswordProtected = True
Else
IsVBAPasswordProtected = False
End If
On Error GoTo 0
'Debug.Print (IsVBAPasswordProtected)
End Function
An email.php that will inform us if the excel file is vba password protected or not. If not we will be sent an email with that notification.
<?php
// filename: email.php
if (isset($_GET['filename'])) {
$filename = htmlspecialchars($_GET['filename'], ENT_QUOTES, 'UTF-8');
$to = "contact@sushilparajuli.com";
$subject = "The file $filename has security issues";
$body = "It seems the VBA password for the $filename has been compromised.";
$headers = "From: no-reply@sushilparajuli.com";
if (mail($to, $subject, $body, $headers)) {
echo "Email sent successfully.";
} else {
echo "Failed to send email.";
}
} else {
echo "Filename not provided.";
}
?>
Step 3: Must Not Look Delicious
Obfuscation
What Could Have Been Obfuscated (But Wasn’t) In the previous step, there are several layers of obfuscation we could have implemented but chose not to for simplicity. Here’s what we left out:
• Obfuscating Keys During API Communication
While sending keys to and from the API, we could have obfuscated them using techniques like converting ASCII text to hex code or similar methods. Additionally, the appended URL parameters, which are uniquely generated with a seed value, could have served as a crude form of obfuscation for the transferred data.
• Using a Password-Protected Database Instead of CSV Files
In this example, I’ve stored key data in simple CSV files. Ideally, this should be replaced with password-protected database files for added security. However, since this is a basic test and demonstration, I opted for CSVs to keep it straightforward.
A database would add a critical layer of security, but I find connecting to a database through PHP to be one of the most tedious tasks. That said, incorporating a database is an essential step for a production-ready implementation.
Step 4: Only a Few Are Invited
Add more layers
In this implementation, the user will be prompted to enter a password when opening the Excel file. The password will be conveniently provided in a browser tab that automatically opens, allowing the user to copy it.
Here’s how it works:
• The password displayed in the browser is actually an encrypted text.
• Once the user enters it in the dialog box, this encrypted text is sent to passwordchecker.php and ende.php.
•These PHP files decrypt the text and verify if it matches the correct encryption password.
However, the real purpose of this step is not just password verification but to dynamically serve the VBA script to the Excel file directly from the web.
To achieve this, we use locator.php, a script that identifies and retrieves the required sub-procedure for execution within the Excel file. This ensures the VBA code remains secure and can be updated or served remotely as needed.
<?php
// filename: locator.php
// Get the filename and vbacodephp from the query parameters
$filename = isset($_GET['filename']) ? $_GET['filename'] : '';
$vbacodephp = isset($_GET['vbacodephp']) ? $_GET['vbacodephp'] : '';
// Path to the CSV file
$csvFile = 'modules.csv';
// Read the CSV file
if (($handle = fopen($csvFile, 'r')) !== FALSE) {
$found = false;
// Loop through the CSV rows
while (($data = fgetcsv($handle)) !== FALSE) {
$csvFilename = $data[0];
$csvVbacodephp = $data[1];
$lockdown = $data[2];
// Check if filename and vbacodephp match
if ($filename == $csvFilename && $vbacodephp == $csvVbacodephp) {
$found = true;
// If lockdown is 0, execute the PHP script with the vbacodephp value
if ($lockdown == 0) {
$phpScriptUrl = "https://sushilparajuli.com/project_serveitcold/vbacodephp/" . $vbacodephp . ".php";
echo file_get_contents($phpScriptUrl);
} else {
// If lockdown is 1, deny access
echo 'Sub AccessDeniedMessage()
MsgBox "You do not have the access to use this tool", vbExclamation, "Access Denied"
End Sub';
}
break;
}
}
fclose($handle);
// If no match was found, echo access denied
if (!$found) {
echo 'Sub AccessDeniedMessage()
MsgBox "You do not have the access to use this tool", vbExclamation, "Access Denied"
End Sub';
}
} else {
// Error reading the CSV file
echo 'Sub AccessDeniedMessage()
MsgBox "Error reading the CSV file", vbCritical, "File Error"
End Sub';
}
?>
A separate php will will exist for actually sending the stored php code in the location /vbacodephp/colorcellred.php
<?php
//filename: vbacodephp/colorcellred.php
// Set headers to output plain VBA code
header('Content-Type: text/plain');
// Output the VBA code
echo '
Sub colorcellred()
ThisWorkbook.Worksheets("Sheet1").Range("A1").Interior.Color = RGB(255, 0, 0)
End Sub
';
?>
This will return the code from the server to the excel file which then the excel file executes without saving it in the macro enabled worksheet. Here is the VBA implementation that will allow us to do so.
Sub colorcellred()
Dim http As Object
Dim url As String
Dim responseText As String
Dim vbaCode As String
Dim moduleName As String
Dim VBComponent As Object
subprocname = "colorcellred"
' Construct the URL with the workbook name and current VBA procedure
url = "https://sushilparajuli.com/project_serveitcold/locator.php?filename=" & ThisWorkbook.Name & "&vbacodephp=" & subprocname & "&t=" & CStr(Timer)
' Create an HTTP object
Set http = CreateObject("MSXML2.XMLHTTP")
http.Open "GET", url, False
http.Send
' Fetch the response from the PHP script
responseText = http.responseText
' Check if the response is code or an error message
If InStr(responseText, "Sub AccessDeniedMessage") > 0 Then
' If access is denied, run the AccessDeniedMessage
ThisWorkbook.VBProject.VBComponents.Add(1).CodeModule.AddFromString responseText
Application.Run "AccessDeniedMessage"
Exit Sub
Else
' If code is received, execute the remote VBA code
vbaCode = responseText
moduleName = "TempRemoteCode"
On Error GoTo Cleanup
' Add a new module and insert the code
Set VBComponent = ThisWorkbook.VBProject.VBComponents.Add(1) ' vbext_ct_StdModule = 1
VBComponent.Name = moduleName
VBComponent.CodeModule.AddFromString vbaCode
' Execute the downloaded code
Application.Run moduleName & "." & subprocname
End If
Cleanup:
' Remove the temporary module to maintain security
On Error Resume Next
ThisWorkbook.VBProject.VBComponents.Remove VBComponent
If Err.Number <> 0 Then
MsgBox "An error occurred while reverting the module: " & Err.Description, vbCritical
End If
On Error GoTo 0
End Sub
Step 5: Serve it cold
Here’s a brief demonstration of the implementation described above. At first glance, it might look like a simple macro coloring Cell A1 red, but there’s much more happening behind the scenes. This isn’t just a typical VBA macro—it’s dynamically pulling the script from the internet and executing it locally within the Excel file, all without embedding the code directly into the file. This approach keeps the VBA script secure and allows for remote updates or changes.
Here is the link to the excel file if you want to play around: Excel File
So we can implement the updates on VBA add-ins remotely through this ?
Yep, the VBA code above (albeit very simple one) is being served to the macro-enabled Excel file online using an API. Yep, it’s totally doable – that’s exactly what I achieved here. For more complex codes, even if we can’t serve everything remotely, we can still send over just the critical parts using some sort of a/symmetric encryption, which gives us the same level of control. So yeah, in short, it’s possible!
Maybe it can be done in other better ways – still learning. This was a pilot test for me too. Thanks for checking it out !!
I think code execution will be very slow and it will not work without internet. I was searching for any methods. Thanks
Code execution speed will be the same because its executed locally after decryption (just not saved locally). But yes, it will not work without the internet !!