Managing Software Licenses

In true nerd fashion. I don’t use a spreadsheet (anymore) to manage the various licenses that I buy for software. CSV files are fine for shorter lists, but become problematic when dealing with more, especially trying to properly format the document. So, I started looking for alternatives.

There are a couple of purpose built programs, generally for enterprises, that claim to manage your software licenses, but I’m not that big, and I don’t have enterprise cash to throw around. So I started looking at SQLite, which is a durable format that even the Library of Congress endorses as a archival format. However, it can be a little obtuse to work with. Plus you can’t just crack open the file in an editor and edit it. So, you need to know SQL and how to work around the command-line tools for it. Still, a tempting choice.

However, there are text-only databases that can be opened and handled by text editors and command-line tooling. The one that comes to mind is recutils, a suite of utilities that can parse, format and error check a database made up of a text-only schema and it’s associated data. It kind of looks like this:

%rec: License
%doc: A collection of personal software licenses that I have purchased.
%typedef: OS_t enum Windows macOS Linux CrossPlatform
%key: Id
%type: Id int
%type: Name line
%type: PurchaseDate date
%type: OS OS_t
%mandatory: Name LicenseKey
%auto: Id

Id: 1
Name: Acorn
Version: 8
PurchaseDate: 1970-01-01
LicenseKey:
OS: macOS

The stuff behind the % are field identifiers, allowing you to define what a field should contain and conform to. You can set your own types, like I did with %Id. Additionally you can set mandatory and unique fields to better wrangle compliance of data being entered. All in all, it’s a pretty damn simple format that works really well. Bash, the shell, also has a built in recread built-in to read in data like this, so if you’re already handling it on the command-line, it’s one less thing you have to run. For me, I can get by with the standard utilities in this ZSH shell script:

#!/usr/bin/env zsh

# VERSION: 2.3

# Hello, future me. This is a mini-zsh program to make working with the software
# license database a bit easier from the command-line. macOS, specifically.
# However, there shouldn't be anything in here that is specific to macOS, except
# maybe the path to the database itself.

# Usage:
## You'll want to set the variables in the '_decrypt-license' and 'license-add'
## functions to the proper path for the encryption key and the license recutils
## file.

# CHANGELOG
## Version 1:
##  Initial creation.
## Version 2:
##  Add encryption and reverse the sort options on fzf
## Version 2.1:
##  Add guard-rails for missing license and keyfiles
## Version 2.1.1:
##  Fix comparison for missing files (&& vs ||). Thanks ChatGPT!
## Version 2.1.2:
##  Add check for `age` tool
## Version 2.2:
##  Not really a fix in this file, but fixes to the database allow this to work
## Version 2.3:
##  Add purchase date field and pre-check for 'age' command

if (( $+commands[recsel] )) && (( $+commands[age] ));
then	

	# Decrypt the license file with the age key, this will dump the file to
	# stdout, so all subsequent uses should be a pipeline dealing with the
	# output
	_decrypt-license()
	{
		local LICENSE_FILE="path-to-license.rec"
		local KEY_FILE="path-to-encryption-key.txt"

		if ! (( $+commands[age] ));
		then
			echo "Unable to find the `age` command-line tool in the PATH, please install before retrying."
			return 1
		fi

		if ! [[ -e "$KEY_FILE" && -e "$LICENSE_FILE" ]];
		then
			echo "Unable to decrypt the license file, please ensure both the license file exists and the keyfile exists."
			echo "Key File: $KEY_FILE"
			echo "License File: $LICENSE_FILE"
			return 1
		fi

		age --decrypt --identity "$KEY_FILE" "$LICENSE_FILE"
	}

	# Add to the license file
	license-add()
	{
		local LICENSE_FILE="path-to-license.rec"
		local KEY_FILE="path-to-encryption-key.txt"

		# First, lets get some information from the user
		read "?Software name: " name
		read "?Platform (Windows, macOS, Linux or CrossPlatform): " os
		read "?Purchase date (YYYY-MM-DD): " pdate
		read "?Version number: " version
		read "?License key: " key

		if ! [[ -n $name || -n $os || -n $version || -n $key ]];
		then
			echo "All fields are required!"
			return 1
		fi

		# Decrypt, edit the stream, encrypt.
		cp "$LICENSE_FILE" "$LICENSE_FILE".bkup
		_decrypt-license | \
			recins --verbose \
				   --type=License \
				   -f Name -v "$name" \
				   -f PurchaseDate -v "$pdate" \
				   -f LicenseKey -v "$key" \
				   -f "OS" -v "$os" | \
			age --encrypt --identity "$KEY_FILE" --output "$LICENSE_FILE"
	}

	# Search by name (case-insensitive)
	license-find()
	{
		_decrypt-license | \
			recsel --type=License --case-insensitive \
				   --expression="Name = '$1'" \
				   --print=Name,OS,Version,LicenseKey
	}

	# Search by OS
	license-os() {
		_decrypt-license | \
			recsel --type=License --case-insensitive \
				   --expression "OS = '$1'" \
				   --print-row=Name
	}

	if (( $+commands[fzf] ));
	then
		license-fzf() {
			local selected=$(_decrypt-license | \
								recsel --collapse --type=License \
										--print-values=Name | \
								fzf --height 10 --reverse)
			if [[ -n "$selected" ]];
			then
				license-find "$selected"
			else
				return
			fi
		}
	fi
fi

Here, I use recutils (recsel and recins) to manage the actual database, and age to encrypt/decrypt the data. You could, of course, use whatever encryption system you want to handle the data at rest. The nice thing here is that age (and GNUPG) support reading data from stdin and writing to stdout, which makes it perfect for manipulating the database in-memory, without having to do any jiggery-pokery with temp files and risking data being left somewhere unencrypted. Much like pass, this lets each tool do what it does best, and just strings each tool along, passing the relevant data to/from the user as needed. There’s even a handy filtering tool in fzf to help the user find an entry that they might not know the name of off-hand.

Anyway, I hope this helps someone build their own license system, or other cool tool. Let me know if it helped!

Respond via email.