This article is about browsing history and focuses on the commonly used browsers: Edge and Chrome. In this article I will explain how to get the recent browsing history, in this case, the history of the last 10 minutes, along with the dates and times in UTC.
Github repo: https://github.com/securedglobe/browsinghistory
Introduction
When trying to get the browsing history, there are several questions:
- Where is it stored?
- How frequent is it updated?
- In what format is it stored?
- What is the best method to access that information?
Microsoft Edge
Microsoft Edge stores its history in "<User profile>\AppData\Local\Microsoft\Edge\User Data\Default\History". It uses a table named urls to store each visit, excluding Incognito mode.
From the programming view, if you have found the user's profile path and stored it in userProfilePath, the database path will be:
userProfilePath + "\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default\\History"
Chrome
Google Chrome stores its history in "<User profile>\AppData\Local\Google\Chrome\User Data\Default\History". It uses a table named urls to store each visit, excluding Incognito mode.
From the programming view, if you have found the user's profile path and stored it in userProfilePath, the database path will be:
userProfilePath + "\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\History"
The building blocks
For the purpose of this article, I created a Console application using Visual Studio 2022.
Database
Since the browsing history is stored in an sqlite3 database, we need to include the sqlite3 library or source code files.
To be able to read the browsing history database, we need to be able to handle sqlite3 databases. You can find further information about sqlite3 here. We then need to add sqlite3.c and sqlite3.h to the project.
User's Profile Folder
All locations are relative to the current user's profile path in the c:\users folder. We first need to find that path. Here is a function that does that:
std::wstring GetUserProfilePath()
{
WCHAR path[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_PROFILE, NULL, 0, path)))
{
return std::wstring(path);
}
return L"";
}
Using the database while being locked
Since the databases may be locked, and they will be locked if Edge or Chrome are running, we first need to copy them to another location and access them from there.
bool CopyDatabaseToTemp(const std::wstring& dbPath, std::wstring& tempDbPath)
{
wchar_t tempPath[MAX_PATH];
if (GetTempPathW(MAX_PATH, tempPath) == 0)
{
return false;
}
wchar_t tempFileName[MAX_PATH];
if (GetTempFileNameW(tempPath, L"dbcopy", 0, tempFileName) == 0)
{
return false;
}
tempDbPath = std::wstring(tempFileName);
try
{
std::filesystem::copy_file(dbPath, tempDbPath, std::filesystem::copy_options::overwrite_existing);
return true;
}
catch (const std::filesystem::filesystem_error& e)
{
wprintf(L"Failed to copy database: %s\n", ConvertUtf8ToWide(e.what()).c_str());
return false;
}
}
Handling Unix dates and times
There are several ways to store dates and times.
WebKit Epoch
The WebKit timestamp starts from January 1, 1601 (UTC).
Unix Epoch
The Unix timestamp starts from January 1, 1970 (UTC).
Converting between WebKit Epoch to Unix Epoch
We use the following function for the conversion. ConvertWebKitToUnixTime()
Here is how it works:
Parameters:
webkitTime: This is an int64_t value representing a timestamp in microseconds since the WebKit epoch (January 1, 1601).
Return Type:
The function returns a time_t value, which represents the Unix timestamp in seconds since the Unix epoch (January 1, 1970).
The Logic:
The WebKit timestamp is in microseconds, while the Unix timestamp is in seconds. By dividing webkitTime by 1,000,000, we convert microseconds to seconds.
Adjusting for the Epoch Difference:
The difference between the two epochs (January 1, 1601, and January 1, 1970) is 369 years.
This difference translates to 11644473600 seconds.
The - 11644473600LL in the calculation adjusts the timestamp from the WebKit epoch to the Unix epoch.
Final Computation:
static_cast<time_t>(webkitTime / 1000000 - 11644473600LL);
It takes the WebKit timestamp in microseconds, converts it to seconds, and then subtracts the difference in seconds between the two epochs to yield the correct Unix timestamp.
Here is the function:
time_t ConvertWebKitToUnixTime(int64_t webkitTime)
{
return static_cast<time_t>(webkitTime / 1000000 - 11644473600LL);
}
There is also some code for printing the results in human readable format, in my case, I display them in UTC.
std::wstring FormatUnixTimeToUTC(time_t unixTime)
{
struct tm timeInfo;
if (gmtime_s(&timeInfo, &unixTime) != 0)
{
return L"Invalid time";
}
wchar_t buffer[80];
wcsftime(buffer, sizeof(buffer), L"%Y-%m-%d %H:%M:%S", &timeInfo);
return std::wstring(buffer);
}
Printing the recent browsing history
In the source code below, we print both Edge and Chrome's recent browsing history.
void PrintUrlsFromDatabase(const std::wstring& dbPath, const time_t currentTime, const time_t timeRangeInSeconds)
{
std::wstring tempDbPath;
if (!CopyDatabaseToTemp(dbPath, tempDbPath))
{
wprintf(L"Failed to copy database to temporary file: %s\n", dbPath.c_str());
return;
}
sqlite3* db;
if (sqlite3_open16(tempDbPath.c_str(), &db) != SQLITE_OK)
{
wprintf(L"Failed to open database: %s\n", tempDbPath.c_str());
return;
}
const char* query = "SELECT u.url, v.visit_time FROM urls u JOIN visits v ON u.id = v.url ORDER BY v.visit_time DESC;";
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(db, query, -1, &stmt, nullptr) != SQLITE_OK)
{
wprintf(L"Failed to prepare statement: %S\n", sqlite3_errmsg(db));
sqlite3_close(db);
return;
}
while (sqlite3_step(stmt) == SQLITE_ROW)
{
const char* foundUrlUtf8 = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
int64_t visitTimeWebKit = sqlite3_column_int64(stmt, 1);
time_t visitTimeUnix = ConvertWebKitToUnixTime(visitTimeWebKit);
if (difftime(currentTime, visitTimeUnix) <= timeRangeInSeconds)
{
std::wstring foundUrl = ConvertUtf8ToWide(foundUrlUtf8);
std::wstring visitTimeStr = FormatUnixTimeToUTC(visitTimeUnix);
wprintf(L"URL: %s, Visit Time (UTC): %s\n", foundUrl.c_str(), visitTimeStr.c_str());
}
}
sqlite3_finalize(stmt);
sqlite3_close(db);
std::filesystem::remove(tempDbPath);
}