Home Page
Home
Search:

Welcome to the digital realm of Jeffrey Riaboy. Here, you’ll find a curated collection of my endeavors, musings, updates, and assorted reflections. As a self-taught programmer and avid computer enthusiast, I work in C++ as a favorite choice, though the realities of our digital world have led me to become proficient in dozens of other languages.

Programming has been a lifelong journey for me which I have been hacking on since before I can rememember. It is my passion, profession, first love, and my constant challenge, offering both fulfillment and frustration in equal measure.

This space is dedicated to sharing insights, innovations, and inspirations I’ve gathered along the way. My aim is for you to discover something here that sparks your interest or serves your needs, as that is the driving force behind my commitment to compile and share this content. Dive in and explore. Your presence is highly appreciated.

Dakusan~
The original (well... last) intro page to my website before this became the home. It is a flash portal to my personal sites of the past.
Intro
[1999-2001?] My ancient NES emulator made in Visual Basic (which was made to prove the power and flexibility [not speed] of the language).
HyNes
[2002] A chronicle of my experiences and tinkering from early ’02 to early ’04 on an addictive yet horribly crappy MMORPG. Site also has some nice “hacking”/reverse engineering tutorials.
Ragnarok Hacking
I’ve temporarily set this to link to the Projects section of this website until I’m ready to announce the new website this will link to.
Projects
Updates Archive
Updated:04/15/24
The nulltypes system has been overhauled so all null types are under a generic type named NullType in the top level package. For example, instead of using nulltypes.NullUint8 you would now use NullType[uint8]. This also really helped clean up the null types code. This is a version breaking change, hence the minor version number update.

Other minor changes:
  • Readme file and package information has been updated with the following changes:
    • The type support section has been redone for clarity
    • The structs in the code examples have had the members labeled to explain their used supported type
  • Marshled JSON strings are now properly json escaped
  • Added bypass for my RawBytes bug fix, now that it has been fixed in go v1.23
  • Removed test case that is no longer compatible with go 1.21+
Download Content
Section: Misc > Resume
Updated:03/23/24

I made a pretty massive overhaul of my resume. Both the PDF and the copy on this site’s resume section have been updated.

Download Content
Updated:03/16/24

GoFasterSQL is a tool designed to enhance the efficiency and simplicity of scanning SQL rows into structures in Go[lang].

While this project was first released in December of 2023 on github and has already had 7 releases there, I’ve finally gotten around to adding it here. I don’t anticipate there being many more releases any time soon as the project is feature complete.

Download Content
Post Archive
RSS Feed
Password transfer script for Roboform to BitWarden
I created this script because I wasn’t happy with the native Roboform importer inside Bitwarden. This fixed multiple problems including:
* Ignoring MatchUrls
* Parent folders weren’t created if they had no items in them (e.x. if I had identities in Financial/Banks, but nothing in /Financial, then it wouldn’t be created /Financial)
* Completely ignoring the extra fields (RfFieldsV2)


This fixes all those problems.
This needs to be ran via php in a command line interface, so `php convert.php`. It requires the [bitwarden cli “bw” command](https://bitwarden.com/help/cli/). There are 2 optional arguments you can pass: 1) The name of the import file. If not given, `./RfExport.csv` will be used 2) The “bw” session token. If not given as a parameter, it can be set directly inside this file on line #4. You get this token by running `bw unlock` (after logging in with `bw login`).
This script does the following: * Reads from a csv file exported by Roboform to import into bitwarden * Runs `bw sync` before processing * Imported Fields: * Name: Becomes the name of the item * Url: Becomes the URL for the item * MatchUrl: If this is not the same “Url” then it is added as another URL for the item * Login: Becomes the login (user) name * Pwd: Becomes the password * Note: Becomes the note * Folder: Item is put in this folder * RfFieldsV2 (multiple): * Fields that match the login or password fields are marked as “linked fields” to those. If the field names are the following, they are not added as linked fields: *user, username, login, email, userid, user id, user id$, user_email, login_email, user_login, password, passwd, password$, pass, user_password, login_password, pwd, loginpassword* * Fields that are different than the login or password are added as appropriate “Custom fields”. Supported types: '', rad, sel, txt, pwd, rck, chk, are * Each field has 5 values within it, and 3 of those seem to be different types of names. So as long as they are not duplicates of each other, each name is stored as a seperate field. * If all fields are blank but “name” and “note” then the item is considered a “Secure Note” * While reading the csv file for import, errors and warnings are sent to stdout * After the csv import has complete, it will give a total number of warnings/errors and ask the user if they want to continue * Creates missing folders (including parents that have no items in them) * During export to bitwarden: * This process can be quite slow since each instance of running “bw” has to do a lot of work * Keeps an active count of items processed and the total number of items * If duplicates are found (same item name and folder) then the user is prompted for what they want to do. Which is either s=skip or o=overwrite. A capitol of those letters can be given to perform the same action on all subsequent duplicates. * Errors are sent to stdout


Download script here
<?php
global $BW_SESSION;
/** @noinspection SpellCheckingInspection,RedundantSuppression */
$BW_SESSION=($argv[2] ?? 'FILL_ME_IN');

print RunMe($argv)."\n";

//Make sure “bw” is installed, given session is valid, and is synced
function CheckBWStatus(): ?string
{
	if(is_string($StatusCheck=RunBW('status')))
		return $StatusCheck;
	else if(!is_object($StatusCheck) || !isset($StatusCheck->status))
		return 'bw return does not have status';
	else if($StatusCheck->status==='locked')
		return 'bw is locked';
	else if($StatusCheck->status!=='unlocked')
		return 'bw status is invalid';

	$ExpectedMessage='Syncing complete.';
	if(is_string($SyncCheck=RunBW('sync', null, false)))
		return $SyncCheck;
	else if($SyncCheck[0]!==$ExpectedMessage)
		return "Sync expected “${ExpectedMessage}” but got “${SyncCheck[0]}”";

	return null;
}

//Pull the known folders
function PullKnownFolders(): string|array
{
	$FolderIDs=[];
	if(is_string($FolderList=RunBW('list folders')))
		return $FolderList;
	foreach($FolderList as $Folder)
		$FolderIDs[$Folder->name]=$Folder->id;
	unset($FolderIDs['No Folder']);
	return $FolderIDs;
}

//Get the file handle and the column indexes
function GetFileAndColumns(): string|array
{
	//Prepare the import file for reading
	ini_set('default_charset', 'UTF-8');
	$FileName=$argv[1] ?? './RfExport.csv';
	if(!file_exists($FileName))
		return 'File not found: '.$FileName;
	else if(!($f=fopen($FileName, 'r')))
		return 'Error opening file: '.$FileName;

	//Check the header row values
	if(!($HeadRow=fgetcsv($f)))
		return 'Error opening file: '.$FileName;
	else if(!count($HeadRow) || $HeadRow[0]===NULL)
		return 'Missing head row: '.$FileName;
	if(str_starts_with($HeadRow[0], "\xEF\xBB\xBF")) //Remove UTF8 BOM
		$HeadRow[0]=substr($HeadRow[0], 3);
	unset($FileName);

	$ExpectedCols=array_flip(['Url', 'Name', 'MatchUrl', 'Login', 'Pwd', 'Note', 'Folder', 'RfFieldsV2']);
	$ColNums=[];
	foreach($HeadRow as $Index => $Name) {
		if(!isset($ExpectedCols[$Name]))
			if(isset($ColNums[$Name]))
				return 'Duplicate column title: '.$Name;
			else
				return 'Unknown column title: '.$Name;
		$ColNums[$Name]=$Index;
		unset($ExpectedCols[$Name]);
	}
	if(count($ExpectedCols))
		return 'Required columns not found: '.implode(', ', array_keys($ExpectedCols));
	else if($ColNums['RfFieldsV2']!==count($ColNums)-1)
		return 'RfFieldsV2 must be the last column';

	return [$f, $ColNums];
}

//Process the rows
function ProcessRows($f, array $ColNums, array &$FolderIDs): array
{
	$Counts=['Error'=>0, 'Warning'=>0, 'Success'=>0];
	$RowNum=0;
	$FolderNames=[];
	$Items=[];
	while($Line=fgetcsv($f, null, ",", "\"", "")) {
		//Process the row and add result type to counts
		$RowNum++;
		$Result=ProcessRow($Line, $ColNums);
		$Counts[$Result[0]]++;

		//Handle errors and warnings
		if($Result[0]!=='Success') {
				print "$Result[0]: Row #$RowNum $Result[1]\n";
				continue;
		} else if(isset($Result['Warnings']) && count($Result['Warnings'])) {
			print "Warning(s): Row #$RowNum ".implode("\n", $Result['Warnings'])."\n";
			$Counts['Warning']+=count($Result['Warnings']);
		}

		//Add the folder to the list of folders (strip leading slash)
		$FolderName=($Line[$ColNums['Folder']] ?? '');
		$FolderName=substr($FolderName, ($FolderName[0] ?? '')==='/' ? 1 : 0);
		$Result['Data']->folderId=&$FolderIDs[$FolderName];
		$FolderNames[$FolderName]=1;

		//Save the entry
		$Items[]=$Result['Data'];
	}
	fclose($f);
	return [$Counts, $FolderNames, $Items, $RowNum];
}

//Process a single row
function ProcessRow($Line, $ColNums): array
{
	//Skip blank lines
	if(!count($Line) || (count($Line)===1 && $Line[0]===null))
		return ['Warning', 'is blank'];

	//Extract the columns by name
	$C=(object)[];
	foreach($ColNums as $Name => $ColNum)
		$C->$Name=$Line[$ColNum] ?? '';

	//Check for errors and end processing early for notes
	if($C->Name==='')
		return ['Error', 'is missing a name'];
	else if($C->Url==='' && $C->MatchUrl==='' & $C->Login==='' && $C->Pwd==='') {
		return ['Success', 'Data'=>(object)['type'=>2, 'notes'=>$C->Note, 'name'=>$C->Name, 'secureNote'=>['type'=>0]]];
	}

	//Create a login card
	$Ret=(object)[
		'type'=>1,
		'name'=>$C->Name,
		'notes'=>$C->Note===''  ? null : $C->Note,
		'login'=>(object)[
			'uris'=>[(object)['uri'=>$C->Url]],
			'username'=>$C->Login,
			'password'=>$C->Pwd,
		],
	];
	if($C->MatchUrl!==$C->Url)
		$Ret->login->uris[]=(object)['uri'=>$C->MatchUrl];

	//Create error string for mist fields
	$i=0; //Declared up here so it can be used in $FormatError
	$FormatError=function($Format, ...$Args) use ($ColNums, &$i): string {
		return 'RfFieldsV2 #'.($i-$ColNums['RfFieldsV2']+1).' '.sprintf($Format, ...$Args);
	};

	//Handle misc/extra (RfFieldsV2) fields
	$Warnings=[];
	for($i=$ColNums['RfFieldsV2']; $i<count($Line); $i++) {
		//Pull in the parts of the misc field
		if(count($Parts=str_getcsv($Line[$i], ",", "\"", ""))!==5)
			return ['Error', $FormatError('is not in the correct format')];
		$Parts=(object)array_combine(['Name', 'Name3', 'Name2', 'Type', 'Value'], $Parts);
		if(!trim($Parts->Name))
			return ['Error', $FormatError('invalid blank name found')];

		//Figure out which “Names” to process
		$PartNames=[trim($Parts->Name)];
		foreach([$Parts->Name2, $Parts->Name3] as $NewName)
			if($NewName!=='' && !in_array($NewName, $PartNames))
				$PartNames[]=$NewName;

		//Process the different names
		foreach($PartNames as $PartName) {
			//Determined values for the item
			$LinkedID=null;
			$PartValue=$Parts->Value;
			/** @noinspection PhpUnusedLocalVariableInspection */ $Type=null; //Overwritten in all paths

			//Handle duplicate usernames and password fields
			if(($IsLogin=($PartValue===$C->Login)) || $PartValue===$C->Pwd) {
				if($Parts->Type!==($IsLogin ? 'txt' : 'pwd'))
					$Warnings[]=$FormatError('expected type “%s” but got “%s”', $IsLogin ? 'txt' : 'pwd', $Parts->Type);
				$Type=3;
				$PartValue=null;
				$LinkedID=($IsLogin ? 100 : 101);

				//If a common name then do not add the linked field
				/** @noinspection SpellCheckingInspection */
				if(in_array(
					strToLower($PartName),
					$IsLogin ?
						['user', 'username', 'login', 'email', 'userid', 'user id', 'user id$', 'user_email', 'login_email', 'user_login'] :
						['password', 'passwd', 'password$', 'pass', 'user_password', 'login_password', 'pwd', 'loginpassword']
				))
					continue;
			} else {
				//Convert the type
				switch($Parts->Type) {
					case '': //For some reason some text fields have no type given
					case 'rad':
					case 'sel':
					case 'txt': $Type=0; break;
					case 'pwd': $Type=1; break;
					case 'rck': //Radio check?
						$Type=0;
						if($PartName!==$Parts->Name) //Ignore second names for radio checks
							continue 2;
						if(count($RadioParts=explode(':', $PartName))!==2)
							return ['Error', $FormatError('radio name needs 2 parts separated by a colon')];
						$PartName=$RadioParts[0];
						$PartValue=$RadioParts[1];
						break;
					case 'chk':
						$Type=2;
						if($PartValue==='*' || $PartValue==='1')
							$PartValue='true';
						else if($PartValue==='0')
							$PartValue='false';
						else
							return ['Error', $FormatError('invalid value for chk type “%s”', $PartValue)];
						break;
					case 'are': //This seems to be a captcha
						continue 2;
					default:
						return ['Error', $FormatError('invalid field type “%s”', $Parts->Type)];
				}
			}

			//Create the return object
			if(!isset($Ret->fields))
				$Ret->fields=[];
			$Ret->fields[]=(object)[
				'name'=>$PartName,
				'value'=>$PartValue,
				'type'=>$Type,
				'linkedId'=>$LinkedID,
			];
		}
	}

	//Return finished item and warnings
	if(count($Warnings))
		$Warnings[0]="RfFieldsV2 warnings:\n".$Warnings[0];
	return ['Success', 'Data'=>$Ret, 'Warnings'=>$Warnings];
}

//Ask the user if they want to continue
function ConfirmContinue($Counts, $RowNum): ?string
{
	//Ask the user if they want to continue
	printf("Import from spreadsheet is finished. Total: %d; Successful: %d, Errors: %d, Warnings: %d\n", $RowNum, $Counts['Success'], $Counts['Error'], $Counts['Warning']);
	while(1) {
		print 'Do you wish to continue the export to bitwarden? (y/n): ';
		fflush(STDOUT);
		switch(trim(strtolower(fgets(STDIN)))) {
			case 'n':
				return 'Exiting';
			case 'y':
				break 2;
		}
	}

	return null;
}

//Get folder IDs and create parent folders for children
function GetFolders($FolderNames, &$FolderIDs): ?string
{
	foreach(array_keys($FolderNames) as $FolderName) {
		//Skip “No Folder”
		if($FolderName==='') {
			$FolderIDs[$FolderName]='';
			continue;
		}

		//Check each part of the folder tree to make sure it exists
		$FolderParts=explode('/', $FolderName);
		$CurPath='';
		foreach($FolderParts as $Index => $FolderPart) {
			$CurPath.=($Index>0 ? '/' : '').$FolderPart;

			//If folder is already cached then nothing to do
			if(isset($FolderIDs[$CurPath])) {
				continue;
			}

			//Create the folder
			print "Creating folder: $CurPath\n";
			if(is_string($FolderInfo=RunBW('create folder '.base64_encode(json_encode(['name'=>$CurPath])), 'create folder: '.$CurPath)))
				return $FolderInfo;
			else if (!isset($FolderInfo->id))
				return "bw folder create failed for “${CurPath}”: Return did not contain id";
			$FolderIDs[$CurPath]=$FolderInfo->id;
		}
	}
	return null;
}

//Pull the known items
function PullKnownItems($FoldersByID): string|array
{
	$CurItems=[];
	if(is_string($ItemList=RunBW('list items')))
		return $ItemList;
	foreach($ItemList as $Item) {
		$CurItems[($Item->folderId===null ? '' : $FoldersByID[$Item->folderId].'/').$Item->name]=$Item->id;
	}
	return $CurItems;
}

function StoreItems($Items, $CurItems, $FoldersByID): int
{
	print "\n"; //Give an extra newline before starting the processing
	$NumItems=count($Items);
	$NumErrors=0;
	$HandleDuplicatesAction=''; //The "always do this" action
	foreach($Items as $Index => $Item) {
		//Clear the current line and print the processing status
		printf("\rProcessing item #%d/%d", $Index+1, $NumItems);
		fflush(STDOUT);

		//If the item already exists then request what to do
		$FullItemName=($Item->folderId===null ? '' : $FoldersByID[$Item->folderId].'/').$Item->name;
		if(isset($CurItems[$FullItemName])) {
			//If no "always" action has been set in $HandleDuplicatesAction then ask the user what to do
			if(!($Action=$HandleDuplicatesAction)) {
				print "\n";
				while(1) {
					print "A duplicate at “${FullItemName}” was found. Choose your action (s=skip,o=overwrite,S=always skip,O=always overwrite):";
					fflush(STDOUT);
					$Val=trim(fgets(STDIN));
					if(in_array(strtolower($Val), ['s', 'o']))
						break;
				}
				$Action=strtolower($Val);
				if($Val!==$Action) //Upper case sets the "always" state
					$HandleDuplicatesAction=$Action;
			}

			//Skip the item
			if($Action==='s')
				continue;

			//Overwrite the item
			if(is_string($Ret=RunBW('edit item '.$CurItems[$FullItemName].' '.base64_encode(json_encode($Item)), 'edit item: '.$FullItemName))) {
				$NumErrors++;
				printf("\nError on item #%d: %s\n", $Index+1, $Ret);
			}
			continue;
		}

		//Create the item
		if(is_string($Ret=RunBW('create item '.base64_encode(json_encode($Item)), 'create item: '.$FullItemName))) {
			$NumErrors++;
			printf("\nError on item #%d: %s\n", $Index+1, $Ret);
		}
	}
	return $NumErrors;
}

//Run a command through the “bw” application
function RunBW($Command, $Label=null, $DecodeJSON=true): string|object|array
{
	//$Label is set to $Command if not given
	if($Label===null)
		$Label=$Command;

	//Run the command and check the results
	global $BW_SESSION;
	exec(sprintf('BW_SESSION=%s bw %s --pretty 2>&1', $BW_SESSION, $Command), $Output, $ResultCode);
	if($ResultCode===127)
		return 'bw is not installed';
	else if($ResultCode===1)
		return "bw “${Label}” threw an error: \n".implode("\n", $Output);
	else if($ResultCode!==0)
		return "bw “${Label}” returned an invalid status code [$ResultCode]: \n".implode("\n", $Output);
	else if($Output[0]==='mac failed.')
		return 'Invalid session ID';
	else if(!$DecodeJSON)
		return [implode("\n", $Output)];
	else if(($JsonRet=json_decode(implode("\n", $Output)))===null)
		return "bw “${Label}” returned non-json result";

	//Return the json object
	return $JsonRet;
}

function RunMe($argv): string
{
	//Make sure “bw” is installed and given session is valid
	if(is_string($Ret=CheckBWStatus()))
		return $Ret;

	//Pull the known folders
	if(is_string($FolderIDs=PullKnownFolders()))
		return $FolderIDs;

	//Get the file handle and the column indexes
	if(is_string($Ret=GetFileAndColumns()))
		return $Ret;
	[$f, $ColNums]=$Ret;
	unset($argv);

	//Process the rows and ask the user if they want to continue
	[$Counts, $FolderNames, $Items, $RowNum]=ProcessRows($f, $ColNums, $FolderIDs);
	if(is_string($Ret=ConfirmContinue($Counts, $RowNum)))
		return $Ret;
	unset($Counts, $RowNum, $f, $ColNums);

	//Get folder IDs and create parent folders for children
	if(is_string($Ret=GetFolders($FolderNames, $FolderIDs)))
		return $Ret;
	unset($FolderNames);

	//Pull the known items
	$FoldersByID=array_flip($FolderIDs);
	if(is_string($CurItems=PullKnownItems($FoldersByID)))
		return $CurItems;

	//Store all items
	$FolderIDs['']=null; //Change empty folder to id=null for json insertions
	$NumErrors=StoreItems($Items, $CurItems, $FoldersByID);

	//Return completion information
	return "\nCompleted ".($NumErrors ? "with $NumErrors error(s)" : 'successfully');
}
Installing KeePassXC & a Flatpak sandboxed browser

Since I have been moving my primary workstation to Linux, I have been in need of finding a new password manager after Roboform failed me. I decided to try out KeePassXC but was having trouble since my Ungoogled Chromium browser was sandboxed in Flatpak. The instructions for this on Unix Stack Exchange were no longer working so I had to dig into it myself.


The 2 primary problems were that:
1) The browsers’ sandboxes did not have the QT libs.
2) The interprocess communication pipe socket had been renamed from kpxc_server to org.keepassxc.KeePassXC.BrowserServer.


The following are the instructions to get KeePassXC running in Flatpak versions of both Chrome and Firefox. This was tested on Linux Mint 21.3 with both Ungoogled Chromium and Firefox. You will need to change the KP_FLATPAK_PACKAGE if you use other versions of Chrome.


  1. Run the relevant below environment variables in your command shell before running the commands in the following steps:
    #Shared environment variables:
    KP_CUSTOM=/home/$USER/keepass-browser
    KP_JSON_NAME=org.keepassxc.keepassxc_browser.json
    
    #Chrome environment variables:
    KP_FLATPAK_PACKAGE=io.github.ungoogled_software.ungoogled_chromium
    KP_JSON_START=~/.config/chromium/NativeMessagingHosts
    KP_JSON_END=~/.var/app/$KP_FLATPAK_PACKAGE/config/chromium/NativeMessagingHosts
    
    #Firefox environment variables:
    KP_FLATPAK_PACKAGE=org.mozilla.firefox
    KP_JSON_START=~/.mozilla/native-messaging-hosts
    KP_JSON_END=~/.var/app/$KP_FLATPAK_PACKAGE/.mozilla/native-messaging-hosts
    		
  2. Install and enable the browser extension:
    KeePassXD > Tools > Settings > Browser Integration:
    • Check “Enable Browser Integration”
    • Check “Chromium” and/or “Firefox”
    • Download the plugin listed on this screen in your browser
    • Click "OK"
    Note: This creates $KP_JSON_START/$KP_JSON_NAME

  3. Set up the needed files in the sandbox:
    #Put KeePass proxy and needed library files in user directory
    mkdir -p $KP_CUSTOM/lib
    mkdir -p $KP_JSON_END #Needed for firefox
    cp /usr/bin/keepassxc-proxy $KP_CUSTOM/
    rsync -a /usr/lib/x86_64-linux-gnu/libicudata* /usr/lib/x86_64-linux-gnu/libicuuc* /usr/lib/x86_64-linux-gnu/libicui* /usr/lib/x86_64-linux-gnu/libdouble* /usr/lib/x86_64-linux-gnu/libsodium* /usr/lib/x86_64-linux-gnu/libQt5* $KP_CUSTOM/lib
    
    #Copy the JSON file to the Flatpak app directory and change the executable path in the file
    cp $KP_JSON_START/$KP_JSON_NAME $KP_JSON_END/
    sed -i "s/\/usr\/bin\//"$(echo $KP_CUSTOM | sed 's_/_\\/_g')"\//" $KP_JSON_END/$KP_JSON_NAME
    		
  4. Add permissions to the Flatpak:
    flatpak override --user --filesystem=$KP_CUSTOM:ro $KP_FLATPAK_PACKAGE #Only required if home directory is not shared to the Flatpak
    flatpak override --user --filesystem=xdg-run/org.keepassxc.KeePassXC.BrowserServer:ro $KP_FLATPAK_PACKAGE
    flatpak override --user --env=LD_LIBRARY_PATH=$(flatpak info --show-permissions $KP_FLATPAK_PACKAGE | grep -oP '(?<=LD_LIBRARY_PATH=).*')";$KP_CUSTOM/lib" $KP_FLATPAK_PACKAGE
    		
Roboform (offline mode) on Linux with Wine
Part 0: Installation

I’ve been moving from Windows to Linux recently and my latest software move attempt is Roboform, a password manager that I use in offline mode. Just running it under Wine works fine for the primary software interaction, but I was unable to get it fully integrated with its chrome extension. Below is the information for my attempt to get it working, in case someone could use the information or wanted to try continuing my attempts.

To move your RoboForm profile to Linux, copy the data from C:\Users\USERNAME\AppData\Local\RoboForm\Profiles\Default Profile to ~/.wine/drive_c/users/USERNAME/Local Settings/Application Data/RoboForm/Profiles/Default Profile.


Part 1: Redirect the extension to the executable

The chrome extension talks to its parent, rf-chrome-nm-host.exe, through the native messaging API. To direct the extension to talk to the windows executable you have to edit ~/.config/chromium/NativeMessagingHosts/com.siber.roboform.json and change the path inside it to /home/USERNAME/chrome-robo.sh, a script file that you will create. You can’t link directly to the rf-chrome-nm-host.exe because it has to be run through Wine, and the path cannot contain arguments.

Create a file with executable permissions at ~/chrome-robo.sh and set its contents to:

cd "/home/USERNAME/.wine/drive_c/Program Files (x86)/Siber Systems/AI RoboForm/9.6.1.1/";
/usr/bin/wine ./rf-chrome-nm-host.exe chrome-extension://pnlccmojcmeohlpggmfnbbiapkmbliob/ --parent-window=0

Part 2: Debugging why it isn’t working

It should have worked at this point, but it still wasn’t, so I had to go further into debug mode. The full copy of the C source code can be found at the bottom of this post. Make sure to replace USERNAME in the source (and instructions in this post) with your username, as using “~” to specify your home directory often doesn’t work in this setup. You may also need to replace version numbers (9.6.1.1 for this post).

First, I created a simple C program to sit in between the chrome extension and the rf-chrome-nm-host.exe. All it did was forward the stdin/stdout between both programs (left=chromium, right=rf-chrome-nm-host.exe) and output their crosstalk to a log file (./log) that I monitored with tail -f. I pointed to the generated executable in the ~/chrome-robo.sh file.

All that was generated on the Linux config was: left=ping, right=confirm ping, left=get product info, END.

I then modified the program to specifically handle chrome native messaging packets, which are always a 4 byte number specifying the packet length, followed by the packet data (IsChrome=1). If this variable is turned off there is a place in the code where you can set a different executable with parameters to run.

Next, I ran the program in Windows with a working left+right config so I could see what packets were expected.

I then added a hack to the program (AddRoboformAnswerHacks=1) to respond to the 2nd-4th packets sent from the left (get-product-info, get-product-info, Initialize2) with what was expected from the right. rf-chrome-nm-host.exe crashed on the 5th packet, and debugging further from there would have taken too more time than I was willing to put in, so at that point I gave up.


Part 3: Trying with windows

I next decided to see if I could get the chromium extension to play nicely by talking directly to the rf-chrome-nm-host.exe on my Windows machine via an SSH tunnel. To do this, I changed the char *cmd[]= line to:

char *cmd[]={
  "/usr/bin/ssh",
  "USERNAME@HOSTNAME",
  "cd c:/Program\\ Files\\ \\(x86\\)/Siber\\ Systems/AI\\ RoboForm/9.6.1.1; ./rf-chrome-nm-host.exe chrome-extension://pnlccmojcmeohlpggmfnbbiapkmbliob/ --parent-window=0",
  NULL
};

While the first 4 packets succeeded in this setup, the following packets (rf-api-request) were all met with: pipe open error: The system cannot find the file specified. (error 2).

This was another stopping point because debugging this would also have taken too long. Though I did do some initial testing using Process Monitor and handle lookups in Process Explorer.


Part 4: The C source code
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>

FILE *SideLog; //This logs all events to the file "log"
FILE *RecordLeft; //This logs the exact packets that the left side sends to the file "left"
int IsChrome=1; //True for roboform chrome compatibility, false to just pass through data as is
int DisplayStatus=1; //Whether to show the "Status: x" updates
int AddRoboformAnswerHacks=0; //Whether to force send fake responses that the rf-chrome server is not responding to
const char* HomePath="/home/USERNAME/.wine/drive_c/Program Files (x86)/Siber Systems/AI RoboForm/9.6.1.1/"; //This must be set.
const char* ExePath="/home/USERNAME/.wine/drive_c/Program Files (x86)/Siber Systems/AI RoboForm/9.6.1.1/rf-chrome-nm-host.exe"; //This must be set. You can make sure rf-chrome-nm-host.exe is running with a 'ps'

//Send back a custom packet to the left. This will show in the log file BEFORE the packet it is responding to.
void SendLeft(char *str)
{
	char Buffer[255];
	int StrSize=strlen(str);
	*(int*)Buffer=StrSize;
	strcpy(Buffer+4, str);

	write(STDOUT_FILENO, Buffer, StrSize+4);
	fprintf(SideLog, "OVERRIDE REQUEST: %s\n\n", Buffer+4);
	fflush(SideLog);
}

//Forward data from left to right or vice versa. Also saves to logs and "left" file
void ForwardSide(int InFileHandle, int OutFileHandle, fd_set* ReadFDS, int IsLeft)
{
	//Exit here if no data
	if(!FD_ISSET(InFileHandle, ReadFDS))
		return;

	//Create a static 1MB+1K buffer - Max packet with chrome is 1MB
	const int BufferLen=1024*(1024+1);
	static char* Buffer=0;
	if(!Buffer)
		Buffer=malloc(BufferLen);

	//If not chrome, just pass the data as is
	char* Side=(IsLeft ? "left" : "right");
	if(!IsChrome) {
		int ReadSize=read(InFileHandle, Buffer, BufferLen);
		write(OutFileHandle, Buffer, ReadSize);
		if(IsLeft)
			fwrite(Buffer, ReadSize, 1, RecordLeft);
		Buffer[ReadSize]=0;
		fprintf(SideLog, "%s (%d): %s\n\n", Side, ReadSize, Buffer);
		fflush(SideLog);
		return;
	}

	//Read the 4 byte packet size and store it at the beginning of the buffer
	unsigned int PacketSize;
	read(InFileHandle, &PacketSize, 4);
	*(unsigned int*)Buffer=PacketSize;

	//Read in the packet and zero it out at the end for the string functions
	read(InFileHandle, Buffer+4, PacketSize);
	Buffer[PacketSize+4]=0;

	//Send fake product-info packet since rf-chrome-nm-host.exe was not responding to it
	if(AddRoboformAnswerHacks && IsLeft && strstr(Buffer+4, "\"name\":\"getProp\"") && strstr(Buffer+4, "\"args\":\"product-info\"")) {
		//The return packet with header at the front
		char VersionInfo[]="{\"callbackId\":\"2\",\"result\":\"{\\\"version\\\":\\\"9-6-1-1\\\",\\\"haveReportAnIssue\\\":true,\\\"haveBreachMon\\\":true,\\\"haveLoginIntoAccount\\\":true}\"}";

		//Simplistic version counter hack since chrome always sends 2 version info requests at the beginning
		static int VersionCount=2;
		VersionInfo[15]='0'+VersionCount;
		VersionCount++;

		SendLeft(VersionInfo);
	//Send fake initialization info packet since rf-chrome-nm-host.exe was not responding to it
	} else if(AddRoboformAnswerHacks && IsLeft && strstr(Buffer+4, "\"name\":\"Initialize2\"")) {
		SendLeft("{\"callbackId\":\"4\",\"result\":\"rf-api\"}");
	//Forward the packet to the other side and store in the "left" file if left side
	} else {
		write(OutFileHandle, Buffer, PacketSize+4);
		if(IsLeft)
			fwrite(Buffer, PacketSize+4, 1, RecordLeft);
	}

	//Output the packet to the log
	fprintf(SideLog, "%s (%d): %s\n\n", Side, PacketSize, Buffer+4);
	fflush(SideLog);
}

int main(void) {
	//Create pipes
	int pipe1[2]; //Parent writes to child
	int pipe2[2]; //Child writes to parent
	if(pipe(pipe1)==-1 || pipe(pipe2)==-1) {
		perror("pipe");
		exit(EXIT_FAILURE);
	}

	//Fork the current process
	pid_t pid = fork();
	if(pid==-1) {
		perror("fork");
		exit(EXIT_FAILURE);
	}

	//New (child) process
	if(pid == 0) {
		//Close unused ends of the pipes
		close(pipe1[1]); // Close write end of pipe1
		close(pipe2[0]); // Close read end of pipe2

		//Redirect stdin to the read end of pipe1
		dup2(pipe1[0], STDIN_FILENO);
		close(pipe1[0]);

		//Redirect stdout to the write end of pipe2
		dup2(pipe2[1], STDOUT_FILENO);
		close(pipe2[1]);

		//Move to the roboform home directory
		if(IsChrome) {
			if(chdir(HomePath) == -1) {
				perror("chdir");
				exit(EXIT_FAILURE);
			}
		}

		//Execute a command that reads from stdin and writes to stdout. The default is the chrome command. If not in chrome, you can fill in the exe and parameter you wish to use
		char *cmd[] = {"/usr/bin/wine", (char*)ExePath, "chrome-extension://pnlccmojcmeohlpggmfnbbiapkmbliob/", "--parent-window=0", NULL};
		if(!IsChrome) {
			cmd[0]="/usr/bin/php";
			cmd[1]="echo.php";
		}
		execvp(cmd[0], cmd);
		perror("execlp");
		exit(EXIT_FAILURE);
	}

	//---Parent process - forwards both sides---
	//Close unused ends of the pipes
	close(pipe1[0]); // Close read end of pipe1
	close(pipe2[1]); // Close write end of pipe2

	//Open the log files
	SideLog = fopen("./log", "w+");
	RecordLeft = fopen("./left", "w+");

	//Run the main loop
	int max_fd=pipe2[0]+1; //Other pipe is STDIN which is 0
	while(1) {
		//Create the structures needed for select
		fd_set read_fds;
		FD_ZERO(&read_fds);
		FD_SET(STDIN_FILENO, &read_fds);
		FD_SET(pipe2[0], &read_fds);

		struct timeval timeout;
		timeout.tv_sec = 10;
		timeout.tv_usec = 0;
	
		//Listen for an update
		int status = select(max_fd, &read_fds, NULL, NULL, &timeout);

		//Display "Status: x" if its setting is true
		if(DisplayStatus) {
			fprintf(SideLog, "Status: %d\n", status);
			if(status==0)
				fflush(SideLog);
		}

		//Exit on bad status
		if (status==-1) {
			perror("select");
			break;
		}

		//Check both sides to see if they need to forward a packet
		ForwardSide(STDIN_FILENO, pipe1[1], &read_fds, 1);
		ForwardSide(pipe2[0], STDOUT_FILENO, &read_fds, 0);
	}

	//Close pipes
	close(pipe1[1]);
	close(pipe2[0]);

	//Wait for the child process to finish
	wait(NULL);

	return EXIT_SUCCESS;
}