librewolf doesn’t work with passkeys

Screen capture of Yubico authentication failure (I deleted my username before I took this capture)

I installed LibreWolf on my Linux Mint system after reading about it on a fellow blogger’s site (see link below). It installed easily, appeared highly performant, and even took the tab fix I’d written about for Firefox nearly two years ago (again, see link below). I wanted to try this alternative to Firefox because of all the recent drama about how Firefox will now handle personal data; it didn’t go down well at all with a lot of users, myself included. But long before all this happened, I frankly got tired of all the limitations I kept running into using Firefox, so I switched to Vivaldi and haven’t looked back. But hope springs eternal, and I still have a soft spot in my heart for Firefox when it was young and pure of heart. So I decided to install LibreWolf and see if it could rekindle a bit of the old Firefox magic.

After following the clear installation instructions for installing LibreWolf on my Linux Mint system, I tried to use LibreWolf to log into my GitHub account using my Yubico security key. That’s when I got the web page at the top of this post. My Yubico key works just fine with Vivaldi, Google Chrome, and original Firefox on my Linux Mint system. Why LibreWolf, which bills itself as a more secure rebuild of Firefox won’t support my Yubico key is beyond me. That lack of full support is a hard failure as far as I’m concerned. I won’t get rid of LibreWolf as I have more than enough disk space to let it sit on the “shelf” while it matures a bit more. I’ll check out any new versions that drop and see if this problem gets fixed. But if you’re like me and use Firefox with a hardware passkey to log into sites that support it, then I’d give a long hard thought about replacing Firefox with LibreWolf, no matter how you might feel about Firefox at the moment. You will not be happy with that decision, I assure you.

Links

Goodbye Firefox, hello Librewolf

LibreWolf — https://librewolf.net

what browser should i use on linux?

Firefox deletes promise to never sell personal data, asks users not to panic — https://arstechnica.com/tech-policy/2025/02/firefox-deletes-promise-to-never-sell-personal-data-asks-users-not-to-panic/

Yubico — https://www.yubico.com

writing a system statitistics application in go

Running pistats on a Raspberry Pi 5 and Linux Ubuntu 22.04.2

I have had a need for a simple computer system monitoring tool that would only monitor the system attributes I cared about. I also wanted that monitoring tool to save its measurements to a log file over time, so that I could mine the logs for any trends. I looked at a number of the top utilities (top, atop, btop++, htop) and pretty much marked them off my list, primarily because they measured too much to suite me. I looked at fastfetch and other such tools and dropped them for the same reason, even though you can tailor them with a config file and/or command line switches to only print what I wanted.

I also had a need to bring my Go programming skills back up to some reasonable level of competency and sophistication. I’ve been coding in Go since I first came across it ten years ago, but I never stuck consistently with it. My involvement varied over time. I felt I needed to brush up on the latest version of Go (which as of this writing is at version 1.24.1) and then begin to write software that would motivate me to learn the breadth and depth of the language and all it had to offer. If nothing else I needed a break from my growing frustrations with Rust.

And so I wrote the utility pistat in Go.

Running pistats on a AMD Ryzen 5 and Linux Mint 22.1

The application is simple enough. It gathers the statistics that matter to me, formats them into an easy-to-read format, and then displays them on the terminal as well as writes them to a log file. Along the way I learned how to read and process certain Linux kernel statics as well as write colored text to the terminal (that was interesting) and to navigate my way into creating and using a log file. Every time pistats is run it appends those statistics to its log file in your home directory.

I originally wrote this for my Raspberry Pis, naively thinking that it would be applicable everywhere. Was I ever wrong about that. There are some “cheats” to get it to work with the AMD (Intel) chip-based system I use as my daily driver, specifically for the hardware platform and the core type. I’m not interested in supporting nearly every hardware platform out there, just the two I care about here at home.

I run it inside a terminal window with a single line of fish script: while true; clear && pistats; sleep 60; end. Then I shrink the terminal down to just large enough to hold the text without wrapping, then put it up in the upper right corner of my desktop and go on about my business.

Although the code is in a single nearly 400-line source file, it isn’t monolithic. Each statistic has a subroutine function that’s called to get the statistics. The report is generated in one function, and then separate functions are called to display in the terminal as well as write to the log file. Every function has a comment block explaining what it does, and there is further commenting as needed internal to each function. It may not be pure idiomatic Go code, but I’m a lot more comfortable with Go (again), and that’s a big win for me.

package mainimport ("bufio""errors""fmt""container/list""log""math""net""os""strconv""strings""syscall""time""github.com/logrusorgru/aurora/v4")// osRelease reads the file /etc/os-release and// filters out the PRETTY_NAME of the OS. It also// trims off any spaces and the double quotes around// the pretty OS name.// It returns the result as a string.func osRelease() string {osrelease, error := os.Open("/etc/os-release")if error != nil {log.Fatal(error)}defer osrelease.Close()scanner := bufio.NewScanner(osrelease)var osname = ""for scanner.Scan() {line := scanner.Text()if strings.Contains(line, "PRETTY_NAME=") {osname = strings.Split(line, "=")[1]osname = strings.Trim(osname, " \"")}}return osname}// kernelVersion reads the file /proc/version and// parses out the kernel version. It also trims// off any spaces.// It returns the result as a string.func kernelVersion() string {procversion, error := os.Open("/proc/version")if error != nil {log.Fatal(error)}defer procversion.Close()scanner := bufio.NewScanner(procversion)var version = ""for scanner.Scan() {version = scanner.Text()version = strings.Split(version, " ")[2]version = strings.TrimSpace(version)}return version}// diskUsage reads the full filesystem size and// the free amount of disk space on the filesystem.// It returns the full filesystem size and free filesystem// size as unsigned 64-bit integers.func diskUsage(path string) (uint64, uint64) {fileSystem := syscall.Statfs_t{}error := syscall.Statfs(path, &fileSystem)if error != nil {log.Fatal(error)}all := fileSystem.Blocks * uint64(fileSystem.Bsize)free := fileSystem.Bfree * uint64(fileSystem.Bsize)return all, free}// uptime reads the system up time at /proc/uptime.// It returns the up time as a formated string.// If the uptime is less than 24 hours, it returns just// the uptime in hours.// If the uptime is greater than or equal to 24 hours,// it returns both days and hours.func uptime() string {procUptime, error := os.Open("/proc/uptime")if error != nil {log.Fatal(error)}defer procUptime.Close()scanner := bufio.NewScanner(procUptime)var line = ""for scanner.Scan() {line = scanner.Text()}var secondsUptimeStr = strings.Split(line, " ")[0]secondsUptimeStr = strings.TrimSpace(secondsUptimeStr)var secondsUptime, _ = strconv.ParseFloat(secondsUptimeStr, 32)const SECONDS_IN_A_DAY float64 = 86400.0const SECONDS_IN_AN_HOUR float64 = 3600.0var daysUptime = math.Floor(secondsUptime / SECONDS_IN_A_DAY)secondsUptime -= daysUptime * SECONDS_IN_A_DAYvar hoursUptime = math.Round(secondsUptime / SECONDS_IN_AN_HOUR)if daysUptime > 0.0 {if daysUptime == 1.0 {return fmt.Sprintf("1 day, %.0f hours", hoursUptime)} else {return fmt.Sprintf("%.0f days, %.0f hours",daysUptime, hoursUptime)}} else {return fmt.Sprintf("%.0f hours", hoursUptime)}}// translateMeminfo translates the string parsed from a meminfo// value to an unsigned 64-bit integer.// Because the values in meminfo are already divided by 1024, or 1KiB,// the value returned is in thousands of bytes, not single bytes.func translateMeminfo(line string) uint64 {var meminfoValue = strings.Split(line, ":")[1]meminfoValue = strings.TrimSpace(meminfoValue)var memoryValue = strings.Split(meminfoValue, " ")[0]memoryValue = strings.TrimSpace(memoryValue)var value, _ = strconv.ParseUint(memoryValue, 10, 64)return value}// memInfo parses out MemTotal, MemAvailable, SwapTotal, and SwapFree// from /proc/meminfo.// Because the values in meminfo are already divided by 1024, or 1KiB,// the values returned are in thousands of bytes, not single bytes.func memInfo() (uint64, uint64, uint64, uint64) {meminfo, error := os.Open("/proc/meminfo")if error != nil {log.Fatal(error)}defer meminfo.Close()scanner := bufio.NewScanner(meminfo)var memoryTotal uint64 = 0var memoryFree uint64 = 0var swapTotal uint64 = 0var swapFree uint64 = 0for scanner.Scan() {line := scanner.Text()if strings.Contains(line, "MemTotal:") {memoryTotal = translateMeminfo(line)}if strings.Contains(line, "MemAvailable:") {memoryFree = translateMeminfo(line)}if strings.Contains(line, "SwapTotal:") {swapTotal = translateMeminfo(line)}if strings.Contains(line, "SwapFree:") {swapFree = translateMeminfo(line)}}return memoryTotal, memoryFree, swapTotal, swapFree}// decodeCorePartNumber decodes the CPU part number found in /proc/cpuinfo// and converts it into the human readable ARM core name.// Note this only works with a Raspberry Pi or equivalent.func decodeCorePartNumber(line string) string {var partNumber = strings.Split(line, ":")[1]partNumber = strings.TrimSpace(partNumber)partNumber = strings.ToUpper(partNumber)switch partNumber {case "0XD01":return "Cortex-A32"case "0XD02":return "Cortex-A34"case "0XD03":return "Cortex-A53"case "0XD04":return "Cortex-A35"case "0XD05":return "Cortex-A55"case "0XD06":return "Cortex-A65"case "0XD07":return "Cortex-A57"case "0XD08":return "Cortex-A72"case "0XD09":return "Cortex-A73"case "0XD0A":return "Cortex-A75"case "0XD0B":return "Cortex-A76"case "0XD0D":return "Cortex-A77"case "0XD0E":return "Cortex-A76AE"default:return "NO MATCH"}}// cpuInfo finds the hardware platform name, core type, and core count// from /proc/cpuinfo.//// The kernel file path /sys/devices/virtual/dmi/id/... is tested to see if// it exists, and if it does, then set the hardware platform name to// the string contents of /sys/devices/virtual/dmi/id/product_name.//// This is a personal test for running this application on an AMD-based// MinisForum tiny computer I own.// Return values are the hardware platform name as a string,// the core type as a string, and core count as an integer.func cpuInfo() (string, string, int) {var platform = ""productname, error :=os.ReadFile("/sys/devices/virtual/dmi/id/product_name")if !errors.Is(error, os.ErrNotExist) {platform = strings.TrimSpace(string(productname))}var coreType = ""var coreCount = 0cpuinfo, error := os.Open("/proc/cpuinfo")if error != nil {log.Fatal(error)}defer cpuinfo.Close()scanner := bufio.NewScanner(cpuinfo)for scanner.Scan() {line := scanner.Text()if strings.Contains(line, "Model") && len(platform) == 0 {platform = strings.Split(line, ":")[1]platform = strings.TrimSpace(platform)}if strings.Contains(line, "model name") && len(coreType) == 0 {coreType = strings.Split(line, ":")[1]coreType = strings.Split(coreType,"w/")[0]coreType = strings.TrimSpace(coreType)}if strings.Contains(line, "CPU part") && len(coreType) == 0 {coreType = decodeCorePartNumber(line)}if strings.Contains(line, "processor") {coreCount++}}if error := scanner.Err(); error != nil {log.Fatal(error)}return platform, coreType, coreCount}// hostIP4 returns the host's IP4 dotted address as a string.func hostIP4() string {connection, error := net.Dial("udp", "8.8.8.8:80")if error != nil {log.Fatal(error)}defer connection.Close()address := connection.LocalAddr().(*net.UDPAddr).String()// The address is a UDP address with a port on the end.// Trim that off along with the ':' character.return strings.Split(address,":")[0]}// generateReport generates a list of all the information we wish to// capture in text format.// Returns a list of text strings.func generateReport() *list.List {currentTime := time.Now()formattedTime := currentTime.Format(time.RFC1123)var hostname, error = os.Hostname()if error != nil {hostname = "UNKNOWN"}var platform, cType, cCount = cpuInfo()var memoryTotalK, memoryFreeK, swapTotalK, swapFreeK = memInfo()const GiBK float64 = 1024.0 * 1024.0var diskTotal, diskFree = diskUsage("/")const GiB float64 = 1024.0 * 1024.0 * 1024.0var rep = list.New()rep.PushBack(fmt.Sprintf("%+22s= %s\n", "Current time", formattedTime))rep.PushBack(fmt.Sprintf("%+22s= %s\n", "Host name", hostname))rep.PushBack(fmt.Sprintf("%+22s= %s\n", "Hardware platform", platform))rep.PushBack(fmt.Sprintf("%+22s= %s / %d\n", "Core type / count", cType, cCount))rep.PushBack(fmt.Sprintf("%+22s= %s\n", "Operating system", osRelease()))rep.PushBack(fmt.Sprintf("%+22s= %s\n", "Kernel version", kernelVersion()))rep.PushBack(fmt.Sprintf("%+22s= %.1f GiB / %.1f GiB\n","Memory total / free",float64(memoryTotalK)/GiBK, float64(memoryFreeK)/GiBK))rep.PushBack(fmt.Sprintf("%+22s= %.1f GiB / %.1f GiB\n","Swap total / free",float64(swapTotalK)/GiBK, float64(swapFreeK)/GiBK))rep.PushBack(fmt.Sprintf("%+22s= %.2f GiB\n","Filesystem capacity", float64(diskTotal)/GiB))rep.PushBack(fmt.Sprintf("%+22s= %.2f GiB\n","Filesystem available", float64(diskFree)/GiB))rep.PushBack(fmt.Sprintf("%+22s= %s\n", "Uptime", uptime()))rep.PushBack(fmt.Sprintf("%+22s= %s\n", "IP Address", hostIP4()))return rep}// displayReport takes a list of strings representing the report generated by// generateReport and reformats them into a format suitable for the terminal// screen, including the incorporation of color.func displayReport(report *list.List) {for line := report.Front(); line != nil; line = line.Next() {if str, ok := line.Value.(string); ok {parts := strings.Split(str, "=")fmt.Printf("%s%s", aurora.Red(parts[0]+":").Bold(), parts[1])}}}// logReport takes a list of strings representing the report generated by// generate report and appends them, unmodified and in the order create by// generateReport, at the end of the log file located in// $HOME/.local/share/pistats/pistats.logfunc logReport(report *list.List) {// We want to know our home directory so we can log our report.// Abort the application if we can't get our home directory.// If we can't get our home directory, then we've got Very Big Problems.userHomeDir, err0 := os.UserHomeDir()if err0 != nil {log.Fatal(err0)}// Check to see if we can create a log file for this application by checking// for $HOME/.local/share/pistat at a minimum. If there is a problem then// silently fail and return.logpath := userHomeDir + "/.local/share/pistat/"_, err1 := os.Stat(logpath)if err1 != nil && os.IsNotExist(err1) {if err2 := os.MkdirAll(logpath, 0770); err2 != nil {return}}// Now try to open the log file. The first time this application is run it// will not exist. If we fail to open the log file, then silently fail and// return.fullpath := logpath + "pistats.log"logfile, err3 :=os.OpenFile(fullpath, os.O_APPEND | os.O_WRONLY | os.O_CREATE, 0644)if err3 != nil {return}defer logfile.Close()// The full log file and path exist and it's open. Append the current report// to the log file. Write a blank line to the end to separate reports.for line := report.Front(); line != nil; line = line.Next() {//fmt.Print(line.Value)logfile.WriteString(line.Value.(string))}logfile.WriteString("\n")}func main() {report := generateReport()displayReport(report)logReport(report)}