I recently moved to Linux and have all my hard drives Luks encrypted, including the primary. I decided to convert my ext4 partitions to Btrfs recently, which I’m totally loving. I also decided to grab another nvme drive and use it as a RAID1 (mirror) drive against my primary drive, using Btrfs’ RAID mechanics. Below are the instructions to accomplish this.
Do note that this is for a situation where you already have a BTRFS volume and want to add a device as RAID1. This assumes you already have your system booting to the LUKS encrypted drive with the root being btrfs. Many modern Linux OS installers can do this for you automatically. Parts of these instructions can still be used in other situations.
- Hopefully you also have a swap partition under the same LVM as your LUKS root (the Linux Mint installer does this by default), as we’ll be using it. If not, you’ll need to modify the instructions. This script resizes the swap partition and adds an “extra” partition to hold your drive key. This is required because a drive key cannot be loaded off your btrfs volume as both drives need to be unlocked first.
- This should be ran from another operating system. I would recommend using Universal USB Installer to do this. It allows you to put multiple OS live cds on a USB key, including optional persistence.
- Run the following script as root (you can use sudo). Make sure to fill in the variables section first. Or even better, run the script 1 line at a time to make sure there are no problems.
#!/bin/bash
#-----------------------------------Variables----------------------------------
#Current root drive
CurPart="nvme0n1p3" #The current drive partition in /dev. This example uses nvme disk #0 partition #3
CurCryptVol="vgmint" #What you named your LVM under LUKS
CurCryptRoot="root" #What you named your root partition under the LVM
CurCryptRootSubVol="/" #The path of the subvolume that is used as the root partition. For example, I use “@”
CurCryptSwap="swap_1" #What you named your swap partition under the LVM
CurCryptExtra="extra" #What you WANT to name your extra partition under the LVM
CurCryptExtraSize="100M" #How big you want your extra partition that will hold your key file
CurKeyPath="" #The path to a key file that will unlock both drives. If left blank then one will be created
#New drive
NewDrive="nvme1n1" #The new drive in /dev. This example uses nvme disk #1
NewPart="nvme1n1p3" #The new partition in /dev. You will be creating this with the cfdisk. This example uses nvme disk#1 partition#3
NewCryptName="raid1_crypt" #What we’ll name the root LUKS partition (no LVM)
#Other variables you do not need to set
CurMount="/mnt/primary"
ExtraMountPath="$CurMount/mnt/extra"
BtrfsReleasePath="kdave/btrfs-progs"
BtrfsReleaseFile="btrfs.box.static"
DriveKeyName="drivekey"
echo "---------------------------------Update BTRFS---------------------------------"
echo "Make sure you are using the latest btrfs-progs"
cd "$(dirname "$(which btrfs)")"
LATEST_RELEASE=$(curl -s "https://api.github.com/repos/$BtrfsReleasePath/releases/latest" | grep tag_name | cut -d \" -f4)
wget "https://github.com/$BtrfsReleasePath/releases/download/$LATEST_RELEASE/$BtrfsReleaseFile"
chmod +x "$BtrfsReleaseFile"
echo "Link all btrfs programs to btrfs.box.static. Rename old files as .old.FILENAME"
if ! [ -L ./btrfs ]; then
for v in $(\ls btrfs*); do
if [ "$v" != "$BtrfsReleaseFile" ]; then
mv "$v" ".old.$v"
ln -s "$BtrfsReleaseFile" "$v"
fi
done
fi
echo "--------------------------Current drive and key setup-------------------------"
echo "Mount the current root partition"
cryptsetup luksOpen "/dev/$CurPart" "$CurCryptVol"
vgchange -ay "$CurCryptVol"
mkdir -p "$CurMount"
mount -o "subvol=$CurCryptRootSubVol" "/dev/$CurCryptVol/$CurCryptRoot" "$CurMount"
echo "If the extra volume has not been created, then resize the swap and create it"
if ! [ -e "/dev/$CurCryptVol/$CurCryptExtra" ]; then
lvremove -y "/dev/$CurCryptVol/$CurCryptSwap"
lvcreate -n "$CurCryptExtra" -L "$CurCryptExtraSize" "$CurCryptVol"
mkfs.ext4 "/dev/$CurCryptVol/$CurCryptExtra"
lvcreate -n "$CurCryptSwap" -l 100%FREE "$CurCryptVol"
mkswap "/dev/$CurCryptVol/$CurCryptSwap"
fi
echo "Make sure the key file exists, if it does not, either copy it (if given in $CurKeyPath) or create it"
mkdir -p "$ExtraMountPath"
mount "/dev/$CurCryptVol/$CurCryptExtra" "$ExtraMountPath"
if ! [ -e "$ExtraMountPath/$DriveKeyName" ]; then
if [ "$CurKeyPath" != "" ]; then
if ! [ -e "$CurKeyPath" ]; then
echo "Not found: $CurKeyPath"
exit 1
fi
cp "$CurKeyPath" "$ExtraMountPath/$DriveKeyName"
else
openssl rand -out "$ExtraMountPath/$DriveKeyName" 512
fi
chmod 400 "$ExtraMountPath/$DriveKeyName"
chown root:root "$ExtraMountPath/$DriveKeyName"
fi
echo "Make sure the key file works on the current drive"
if cryptsetup --test-passphrase luksOpen --key-file "$ExtraMountPath/$DriveKeyName" "/dev/$CurPart" test; then
echo "Keyfile successfully opened the LUKS partition."
#cryptsetup luksClose test #This doesn’t seem to be needed
else
echo "Adding keyfile to the LUKS partition"
cryptsetup luksAddKey "/dev/$CurPart" "$ExtraMountPath/$DriveKeyName"
fi
echo "--------------------------------New drive setup-------------------------------"
echo "Use cfdisk to set the new disk as GPT and add partitions."
echo "Make sure to mark the partition you want to use for the raid disk as type “Linux Filesystem”."
echo "Also make it the same size as /dev/$CurPart to avoid errors"
cfdisk "/dev/$NewDrive"
echo "Encrypt the new partition"
cryptsetup luksFormat "/dev/$NewPart"
echo "Open the encrypted partition"
cryptsetup luksOpen "/dev/$NewPart" "$NewCryptName"
echo "Add the key to the partition"
cryptsetup luksAddKey "/dev/$NewPart" "$ExtraMountPath/$DriveKeyName"
echo "Add the new partition to the root btrfs file system"
btrfs device add "/dev/mapper/$NewCryptName" "$CurMount"
echo "Convert to RAID1"
btrfs balance start -dconvert=raid1 -mconvert=raid1 "$CurMount"
echo "Confirm both disks are in use"
btrfs filesystem usage "$CurMount"
echo "--------------------Booting script to load encrypted drives-------------------"
echo "Get the UUID of the second btrfs volume"
Drive2_UUID=$(lsblk -o UUID -d "/dev/$NewPart" | tail -n1)
echo "Create a script to open your second luks volumes before mounting the partition"
echo "Note: In some scenarios this may need to go into “scripts/local-premount” instead of “scripts/local-bottom”"
cat <<EOF > "$CurMount/etc/initramfs-tools/scripts/local-bottom/unlock_drive2"
#!/bin/sh
PREREQ=""
prereqs()
{
echo "\$PREREQ"
}
case "\$1" in
prereqs)
prereqs
exit 0
;;
esac
. /scripts/functions
cryptroot-unlock
vgchange -ay "$CurCryptVol"
mkdir -p /mnt/keyfile
mount "/dev/$CurCryptVol/$CurCryptExtra" /mnt/keyfile
cryptsetup luksOpen /dev/disk/by-uuid/$Drive2_UUID "$NewCryptName" "--key-file=/mnt/keyfile/$DriveKeyName"
umount /mnt/keyfile
rmdir /mnt/keyfile
mount -t btrfs -o "subvol=$CurCryptRootSubVol" "/dev/$CurCryptVol/$CurCryptRoot" /root
#If you are weird like me and /usr is stored elsewhere, here is where you would need to mount it.
#It cannot be done through your fstab in this setup.
#mount --bind /root/sub/sys/usr /root/usr
mount --bind /dev /root/dev
mount --bind /proc /root/proc
mount --bind /sys /root/sys
EOF
chmod 755 "$CurMount/etc/initramfs-tools/scripts/local-bottom/unlock_drive2"
echo "--------------------Setup booting from the root file system-------------------"
echo "Prepare a chroot environment"
for i in dev dev/pts proc sys run tmp; do
mount -o bind /$i "$CurMount/$i"
done
echo "Run commands in the chroot environment to update initramfs and grub"
chroot "$CurMount" <<EOF
echo "Mount the other partitions (specifically for “boot” and “boot/efi”)"
mount -a
echo "Update initramfs and grub"
update-initramfs -u -k all
update-grub
EOF
echo "-----------------------------------Finish up----------------------------------"
echo "Reboot and pray"
reboot
|
I wanted to make a script to check if updated flatpak app permissions have changed before updating them. I decided to use ChatGPTs newest model, 01-preview for this task and holy crap am I impressed. I had to give it 14 commands to get to the final product and it seems to work great. That number could have been reduced quite a bit had I done things a bit differently.
I still had to find the problems, look up reference docs to correct it, and even debug in an IDE a little. But just telling it where the problems were, it got there in the end, and its user interaction output is way better than what I was planning on doing.
Sections:
My final updated copy of the script
<?php
// -- This script checks for Flatpak updates and reports permission changes. --
// Function to parse permissions from Flatpak metadata
function parsePermissions($content) {
$permissions = [];
$lines = is_array($content) ? $content : explode("\n", $content);
$currentSection = '';
$skipSections = ['Application']; // Sections to skip
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) {
continue;
}
// Check for section headers
if (preg_match('/^\[(.*)\]$/', $line, $matches)) {
$currentSection = $matches[1];
// Skip the [Application] section
if (in_array($currentSection, $skipSections)) {
$currentSection = '';
continue;
}
$permissions[$currentSection] = [];
} elseif ($currentSection !== '') {
// Only process lines within non-skipped sections
$parts = explode('=', $line, 2);
if (count($parts) == 2) {
$key = $parts[0];
$values = explode(';', trim($parts[1], ';'));
$permissions[$currentSection][$key] = $values;
} else {
// Handle keys without '=' (e.g., single permissions)
$permissions[$currentSection][$line] = [];
}
}
}
return $permissions;
}
// Function to compare permissions
function comparePermissions($current, $new) {
$differences = [];
// Get all sections
$sections = array_unique(array_merge(array_keys($current), array_keys($new)));
foreach ($sections as $section) {
$currentSection = isset($current[$section]) ? $current[$section] : [];
$newSection = isset($new[$section]) ? $new[$section] : [];
// Get all keys in this section
$keys = array_unique(array_merge(array_keys($currentSection), array_keys($newSection)));
foreach ($keys as $key) {
$currentValues = isset($currentSection[$key]) ? $currentSection[$key] : [];
$newValues = isset($newSection[$key]) ? $newSection[$key] : [];
// Compare values
$added = array_diff($newValues, $currentValues);
$removed = array_diff($currentValues, $newValues);
if (!empty($added) || !empty($removed)) {
$differences[$section][$key] = [
'added' => $added,
'removed' => $removed,
];
}
}
}
return $differences;
}
// Function to output a line with appId (if given), colored red (IsError=true) or green (IsError=false)
function outputColor($appId, $str, $isError=true, $NoNewline=false)
{
// Determine if coloring should be used
static $hasColors=null;
if(!isset($hasColors)) {
$hasColors=stream_isatty(STDOUT) && (!trim(`command -v tput`) || intval(`tput colors`)>=16);
}
echo
(!$hasColors ? '' : ($isError ? "\e[31m" : "\e[32m")).
($appId ? "$appId: " : '').
$str.
(!$hasColors ? '' : "\e[0m").
($NoNewline ? '' : "\n");
}
// Function to display permission differences
function displayDifferences($appId, $differences) {
outputColor($appId, 'Permission changes:');
foreach ($differences as $section => $keys) {
outputColor(null, " [$section]");
foreach ($keys as $key => $changes) {
if (!empty($changes['added'])) {
outputColor(null, " + $key = " . implode(';', $changes['added']));
}
if (!empty($changes['removed'])) {
outputColor(null, " - $key = " . implode(';', $changes['removed']));
}
}
}
echo "\n";
}
// Function to ask the user a question
function askQuestion($Question) {
outputColor('', "$Question (y/N): ", true, true);
$handle = fopen('php://stdin', 'r');
$line = fgets($handle);
$answer = trim(strtolower($line));
fclose($handle);
return ($answer == 'y' || $answer == 'yes');
}
// Get architecture (moved above the loop)
$archCommand = 'flatpak --default-arch';
exec($archCommand, $archOutput);
$arch = trim(implode('', $archOutput));
// Step 1: Get the list of installed Flatpaks
$installedCommand = 'flatpak list -a --columns=application,branch,origin,options';
exec($installedCommand, $installedOutput);
// Remove header line if present
if (!empty($installedOutput) && strpos($installedOutput[0], 'Application ID') !== false) {
array_shift($installedOutput);
}
// Build an associative array of installed applications
$installedApps = [];
foreach ($installedOutput as $line) {
// The output is tab-delimited
$columns = explode("\t", trim($line));
if (count($columns) >= 4) {
$appId = $columns[0];
$branch = $columns[1];
$origin = $columns[2];
$options = $columns[3];
$installedApps[$appId] = [
'appId' => $appId,
'branch' => $branch,
'origin' => $origin,
'options' => $options,
];
}
}
// Get the list of updates
exec('echo n | flatpak update', $updatesOutput);
foreach ($updatesOutput as $Line) {
if (preg_match('/^ *\d+\.\s+([\w\.-]+)/', $Line, $matches)) {
$updatesAvailable[]=$matches[1];
}
}
// Let the user confirm the updates
echo str_repeat('-', 80)."\n";
echo implode("\n", $updatesOutput)."\n";
echo str_repeat('-', 80)."\n";
if (empty($updatesAvailable)) {
outputColor('', 'No updates available for installed Flatpaks.');
exit(0);
}
if (!askQuestion('Found '.count($updatesAvailable).' updates. Continue?')) {
echo "Updates canceled.\n";
exit(0);
}
$permissionChanges = [];
foreach ($updatesAvailable as $appId) {
if (!isset($installedApps[$appId])) {
outputColor($appId, 'Installed app not found. Skipping.');
continue;
}
$app = $installedApps[$appId];
$branch = $app['branch'];
$origin = $app['origin'];
$options = $app['options'];
// Determine if it's an app or runtime
$isRuntime = strpos($options, 'runtime') !== false;
// Paths to the metadata files
if ($isRuntime) {
$metadataPath = "/var/lib/flatpak/runtime/$appId/$arch/$branch/active/metadata";
} else {
$metadataPath = "/var/lib/flatpak/app/$appId/$arch/$branch/active/metadata";
}
// Check if the metadata file exists
if (!file_exists($metadataPath)) {
outputColor($appId, 'Metadata file not found. Skipping.');
continue;
}
// Read current permissions from the metadata file
$metadataContent = file_get_contents($metadataPath);
$currentPermissions = parsePermissions($metadataContent);
// Get new metadata from remote
$ref = $appId . '/' . $arch . '/' . $branch;
$remoteInfoCommand = 'flatpak remote-info --show-metadata ' . escapeshellarg($origin) . ' ' . escapeshellarg($ref);
// Clear $remoteOutput before exec()
$remoteOutput = [];
exec($remoteInfoCommand, $remoteOutput);
if (empty($remoteOutput)) {
outputColor($appId, 'Failed to retrieve remote metadata. Skipping.');
continue;
}
// Parse new permissions from the remote metadata
$newPermissions = parsePermissions($remoteOutput);
// Compare permissions
$differences = comparePermissions($currentPermissions, $newPermissions);
if (!empty($differences)) {
$permissionChanges[$appId] = $differences;
displayDifferences($appId, $differences);
} else {
outputColor($appId, 'No permission changes found.', false);
}
}
// If there are no permission changes, inform the user
if (empty($permissionChanges)) {
echo "No permission changes detected in the available updates.\n";
}
// Ask user if they want to proceed
if (askQuestion('Do you want to proceed with the updates?')) {
// Proceed with updates
echo "Updating Flatpaks...\n";
passthru('flatpak update -y');
} else {
echo "Updates canceled.\n";
}
?>
List of the changes I made from the AI produced script
- Added “outputColor” function and wrapped appropriate echo statements into it
- Added “-a” to “flatpak list”
- Added output line “Installed app not found. Skipping”
- Added output line “No permission changes found.”
- Added “askQuestion” function since there are now multiple places in the script that ask questions
- Changed get-updates command from “flatpak remote-ls --updates” to “echo n | flatpak update” (with a regex extraction). Also now confirms the update list with the user.
The commands I gave the AI
For demonstration purposes, I have left all of my original typos uncorrected below.
Write me a program in php that checks if there are any updates for my installed flatpaks. and then for each flatpak check and see if the permissions have changed between my current version and the new version. report to me the changes. then ask me if i want to continue with the updates.
your code doesnt work.
first $command = 'flatpak list --app --updates --columns=application,app,branch,origin,installation'; should be flatpak remote-ls --updates --columns=application,app,branch,origin (installation is an invalid column)
and then flatpak info --show-permissions doesnt give the original permissions. you have to get the original non-user-modified permissions from /var/lib/flatpak
the line if (empty($output)) { is incorrect, it should be if (count($output)) { as $output is an array, not a string
$columns = preg_split('/\s{2,}/', trim($line)); is incorrect. its tab delimited, so it just needs to be explode("\t", trim($line));
you also have a problem with the $metadataPath. its not always in /var/lib/flatpak/app/ . It may be in /var/lib/flatpak/runtime/. You can confirm which directory it should be in with the options column having "runtime" as an option.
in the --columns for your first command, "application" should be "name"
actually, you cant get the options from flatpak remote-ls --updates . you have to get the options from flatpak list
flatpak remote-info does not have an option --show-permissions
in your flatpak remote-info command, there is no --branch option.
there is no --assumeno option
there is no --dry-run permission. perhaps use --no-deploy?
you are incorrect. flatpak remote-info does not have an option --show-permissions . we already went over that. perhaps flatpak remote-info --show-metadata to get the permissions
flatpak remote-info does not have --branch
when checking the metadata, do not compare the info under [Application]
also, move the "Get architecture" line above its for loop
you have a problem with exec($remoteInfoCommand, $remoteOutput); you need to always empty $remoteOutput before running the exec. Otherwise, it appends the data to what is already there.
The final script the AI produced
<?php
/**
* This script checks for Flatpak updates and reports permission changes.
*/
// Function to parse permissions from Flatpak metadata
function parsePermissions($content) {
$permissions = [];
$lines = is_array($content) ? $content : explode("\n", $content);
$currentSection = '';
$skipSections = ['Application']; // Sections to skip
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) {
continue;
}
// Check for section headers
if (preg_match('/^\[(.*)\]$/', $line, $matches)) {
$currentSection = $matches[1];
// Skip the [Application] section
if (in_array($currentSection, $skipSections)) {
$currentSection = '';
continue;
}
$permissions[$currentSection] = [];
} elseif ($currentSection !== '') {
// Only process lines within non-skipped sections
$parts = explode('=', $line, 2);
if (count($parts) == 2) {
$key = $parts[0];
$values = explode(';', trim($parts[1], ';'));
$permissions[$currentSection][$key] = $values;
} else {
// Handle keys without '=' (e.g., single permissions)
$permissions[$currentSection][$line] = [];
}
}
}
return $permissions;
}
// Function to compare permissions
function comparePermissions($current, $new) {
$differences = [];
// Get all sections
$sections = array_unique(array_merge(array_keys($current), array_keys($new)));
foreach ($sections as $section) {
$currentSection = isset($current[$section]) ? $current[$section] : [];
$newSection = isset($new[$section]) ? $new[$section] : [];
// Get all keys in this section
$keys = array_unique(array_merge(array_keys($currentSection), array_keys($newSection)));
foreach ($keys as $key) {
$currentValues = isset($currentSection[$key]) ? $currentSection[$key] : [];
$newValues = isset($newSection[$key]) ? $newSection[$key] : [];
// Compare values
$added = array_diff($newValues, $currentValues);
$removed = array_diff($currentValues, $newValues);
if (!empty($added) || !empty($removed)) {
$differences[$section][$key] = [
'added' => $added,
'removed' => $removed,
];
}
}
}
return $differences;
}
// Function to display permission differences
function displayDifferences($appId, $differences) {
echo "Permission changes for $appId:\n";
foreach ($differences as $section => $keys) {
echo " [$section]\n";
foreach ($keys as $key => $changes) {
if (!empty($changes['added'])) {
echo " + $key = " . implode(';', $changes['added']) . "\n";
}
if (!empty($changes['removed'])) {
echo " - $key = " . implode(';', $changes['removed']) . "\n";
}
}
}
echo "\n";
}
// Get architecture (moved above the loop)
$archCommand = 'flatpak --default-arch';
exec($archCommand, $archOutput);
$arch = trim(implode('', $archOutput));
// Step 1: Get the list of installed Flatpaks
$installedCommand = 'flatpak list --columns=application,branch,origin,options';
exec($installedCommand, $installedOutput);
// Remove header line if present
if (!empty($installedOutput) && strpos($installedOutput[0], 'Application ID') !== false) {
array_shift($installedOutput);
}
// Build an associative array of installed applications
$installedApps = [];
foreach ($installedOutput as $line) {
// The output is tab-delimited
$columns = explode("\t", trim($line));
if (count($columns) >= 4) {
$appId = $columns[0];
$branch = $columns[1];
$origin = $columns[2];
$options = $columns[3];
$installedApps[$appId] = [
'appId' => $appId,
'branch' => $branch,
'origin' => $origin,
'options' => $options,
];
}
}
// Get the list of available updates
$updatesCommand = 'flatpak remote-ls --updates --columns=application';
exec($updatesCommand, $updatesOutput);
// Remove header line if present
if (!empty($updatesOutput) && strpos($updatesOutput[0], 'Application ID') !== false) {
array_shift($updatesOutput);
}
// Build a list of applications that have updates
$updatesAvailable = array_map('trim', $updatesOutput);
if (empty($updatesAvailable)) {
echo "No updates available for installed Flatpaks.\n";
exit(0);
}
$permissionChanges = [];
foreach ($updatesAvailable as $appId) {
if (!isset($installedApps[$appId])) {
// The updated app is not in the installed apps list, skip it
continue;
}
$app = $installedApps[$appId];
$branch = $app['branch'];
$origin = $app['origin'];
$options = $app['options'];
// Determine if it's an app or runtime
$isRuntime = strpos($options, 'runtime') !== false;
// Paths to the metadata files
if ($isRuntime) {
$metadataPath = "/var/lib/flatpak/runtime/$appId/$arch/$branch/active/metadata";
} else {
$metadataPath = "/var/lib/flatpak/app/$appId/$arch/$branch/active/metadata";
}
// Check if the metadata file exists
if (!file_exists($metadataPath)) {
echo "Metadata file not found for $appId. Skipping.\n";
continue;
}
// Read current permissions from the metadata file
$metadataContent = file_get_contents($metadataPath);
$currentPermissions = parsePermissions($metadataContent);
// Get new metadata from remote
$ref = $appId . '/' . $arch . '/' . $branch;
$remoteInfoCommand = 'flatpak remote-info --show-metadata ' . escapeshellarg($origin) . ' ' . escapeshellarg($ref);
// Clear $remoteOutput before exec()
$remoteOutput = [];
exec($remoteInfoCommand, $remoteOutput);
if (empty($remoteOutput)) {
echo "Failed to retrieve remote metadata for $appId. Skipping.\n";
continue;
}
// Parse new permissions from the remote metadata
$newPermissions = parsePermissions($remoteOutput);
// Compare permissions
$differences = comparePermissions($currentPermissions, $newPermissions);
if (!empty($differences)) {
$permissionChanges[$appId] = $differences;
displayDifferences($appId, $differences);
}
}
// If there are no permission changes, inform the user
if (empty($permissionChanges)) {
echo "No permission changes detected in the available updates.\n";
}
// Ask user if they want to proceed
echo "Do you want to proceed with the updates? (y/N): ";
$handle = fopen("php://stdin", "r");
$line = fgets($handle);
$answer = trim(strtolower($line));
if ($answer == 'y' || $answer == 'yes') {
// Proceed with updates
echo "Updating Flatpaks...\n";
passthru('flatpak update -y');
} else {
echo "Updates canceled.\n";
}
fclose($handle);
?>
| Yesterday I moved my Plex installation from a Windows machine to a Linux machine. The primary data folders that needed to be copied over were Media, Metadata, Plug-ins, and Plug-in Support. It doesn't hurt to copy over some of the folders in Cache too. It's possible there may be some more data that needs to be moved over, but I don't have documented what.
After moving all the data, I updated the paths in the database for the new machine. Doing this allowed me to keep everything as it was and no new refreshes/scans needed to be done.
The location of the Plex modified SQLite for me was at /usr/lib/plexmediaserver/Plex SQLite. So the following is the bash commands to stop the plex server and open SQLite editor on Linux Mint 21.3.
service plexmediaserver stop
/usr/lib/plexmediaserver/Plex\ SQLite '/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db'
And the following is the SQL I used to replace a D: drive with a path of /home/plex/drives/d/. You can replace these strings in the below code with your custom drives and paths.
#Replace backslashes with forward slash in paths
UPDATE media_parts SET file=REPLACE(file, '\', '/');
UPDATE section_locations SET root_path=REPLACE(root_path, '\', '/');
UPDATE media_streams SET url=REPLACE(url, '\', '/') WHERE url LIKE 'file://%';
#Replace root paths
UPDATE media_parts SET file=REPLACE(file, 'D:/', '/home/plex/drives/d/');
UPDATE section_locations SET root_path=REPLACE(root_path, 'D:/', '/home/plex/drives/d/');
UPDATE media_streams SET url=REPLACE(url, 'file://D:/', 'file:///home/plex/drives/d/') WHERE url LIKE 'file://%';
UPDATE media_streams SET url=REPLACE(url, 'file:///D:/', 'file:///home/plex/drives/d/') WHERE url LIKE 'file://%';
UPDATE metadata_items SET guid=REPLACE(guid, 'file:///D:/', 'file:///home/plex/drives/d/');
|
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');
}
| 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.
-
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
-
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
-
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
-
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
|
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;
}
| After years of saying I’d do it, I'm finally moving my ecosystem from Windows to Linux. After some experimenting and soul searching, I've decided to go with Linux Mint, as it's not exactly Ubuntu with their atrociously horrible decisions, but it provides stability, ease of setup, and a similar enough interface to Windows so as to not lower my productivity.
Getting all of my legacy hardware working, including my 6-monitor setup, was mostly painless, but my beloved Babyface Pro (external professional audio mixer hardware) has been absolute hell to get working. It only natively supports Windows, OSX, and iOS for the “PC” (USB audio passthrough) mode, and the CC mode (class compliant) does not offer my custom waveform transforms. So, my only real option was to use the other analog input interfaces on the Babyface (XLR, SPDIF, or quarter inch audio).
The first hurdle was the power. Normally the device is powered through USB, however if I was going to be using the other audio inputs, I didn't want to leave the USB plugged in all the time, and the Babyface doesn't come with a power adapter. Fortunately, I had a 12V 1A+ power adapter in my big box of random power adapters. The second hurdle was when I discovered that the Babyface does not store the mixer settings when it's powered off. So, every time it gets powered on, it needs to be hooked to another (Windows) machine that can push the mixer settings to it. This also isn't too big a deal as I keep it on a UPS so it generally won't lose power, and if it does, I can use a VM to push the settings.
The next problem was deciding what interface to go through. I was really hoping to use SPDIF/optical since it is a digital signal that does not suffer from interference and degradation, but all the SPDIF interfaces I tried (4 in total) all sounded like garbage. I guess the SPDIF interface on the Babyface is a piece of Junk, which was very disheartening.
My only remaining option was using the analog inputs. I decided to use a mini (3.5mm; 1/8") stereo to quarter inch (6.35mm) mono splitter cord to run into “IN 3/4” and this worked perfectly. However, if the USB interface is plugged in at the same time then this actually creates very audible line noise on the analog inputs within the Babyface itself! This is a horrible design flaw of the device that I was shocked to run into. Fortunately, as mentioned in step 1, I already planned on having the USB cord unplugged, so not a deal breaker.
I first tried the headphone and line out jacks on my motherboard, but the audio quality was only at about 90%. I next tried the line out on my Creative Sound Blaster Audigy from 2014 and the audio was at about 95% quality. It also felt like a cardinal sin to plug in a PCIE 1.0 1x device (0.250 GB/s) into a PCIE 5.0 16x slot (63 GB/s) - lol. So, I bought a Sound Blaster Play 3! USB to mini audio adapter and the audio was perfect! I finally had my setup figured out.
As a fun note, I went to an audiologist a few days ago to have my hearing tested, and the waveform I had devised (through brute force testing) that I had been using through the Babyface for the last 7 years was the exact inverse of the results on the hearing loss frequency chart. |
|