
Clojure and D-Bus
- 》Background
- 》D-Bus
- 》Clojure with D-Bus
- 》Revisting timedate1
- 》KWallet
- 》Getting the kwalletd5 Object
- 》The Rest is Interop - Or How to Use the Wallet
- 》Conclusion
》Background
I was looking for a way to store secrets — specifically web API credentials — in a secure way for use with command-line tools. Using a dotfile is a terrible idea, and rolling my own secure storage is just as bad.
Freedesktop.org has a secret service API which looks like a smarter way to go about this. There is also KDE Wallet Manager, and since I’m using KDE, it might be a good option.
This post is about exploring how to use D-Bus with Clojure, with the eventual goal of storing and retrieving a test secret.
》D-Bus
D-Bus is Linux’s standardized way to handle inter-process communication, including secure comms between applications and the kernel. This nice D-Bus Overview page explains more about it. If you’re not familiar with D-Bus yet, you should give that a quick read before proceeding. Don’t worry about understanding every bit of it.
A whole suite of tools exist for interaction with D-Bus. One of the most
useful is d-feet
which is a GUI for exploring the buses, objects,
and interfaces available on your system. There is also busctl
for
a cli variant.
For simple testing and scripting, dbus-send
can be used. Here’s an
example of getting the current timezone from systemd:
=> dbus-send --system --dest=org.freedesktop.timedate1 \
--print-reply \
/org/freedesktop/timedate1 org.freedesktop.DBus.Properties.Get \
string:org.freedesktop.timedate1 string:Timezone
method return time=1754400037.060292 sender=:1.196828 -> destination=:1.196833 serial=11 reply_serial=2
variant string "America/New_York"
The downside here is that you have to know quite a bit to make that simple call. Let’s break it down:
--system
means to use the system bus, as opposed to the session bus--dest
is the service identifier, in this caseorg.freedesktop.timedate1
/org/freedesktop/timedate1
is the object path
As if that wasn’t tricky enough, the rest is trickier! D-Bus is an RPC
or
Remote Procedure Call system. The objects often have their own interfaces, but
also implement common interfaces such as org.freedesktop.DBus.Properties
.
The Properties
interface provides three standard methods: Get
, GetAll
, and Set
.
And dbus-send has to know what kind of data is being sent, hence the string:
prefix. Continuing on:
org.freedesktop.DBus.Properties.Get
is the method being calledstring:org.freedesktop.timedate1
,string:Timezone
are the parameters
In other words, we’re calling Get
on org.freedesktop.timedate
and getting the
Timezone
field.
I confess that I do not find this straightforward or very "unixy" and that surprised me. Writing the Clojure code was confusing at first because of the way the object interfaces work, and that’s incredibly important to understand.
》Clojure with D-Bus
Since D-Bus is an RPC system, we need a Java implementation that can speak the protocol. The dbus-java package handles that for us!
If you would like to follow along in your REPL, the examples that follow are pasteable. Of course, you need D-Bus libraries. The Leiningin coordinates I used for this are:
[com.github.hypfvieh/dbus-java-core "5.1.1"]
[com.github.hypfvieh/dbus-java-transport-native-unixsocket "5.1.1"]
》Revisting timedate1
To re-create the timedate1 example is easy. Dbus-send handles a few things that have
to be separated out into steps in code. First up is importing the namespaces. We’ll
need the dbus-java DBusConnectionBuilder
as well as the Properties
interface.
(import [org.freedesktop.dbus.connections.impl DBusConnectionBuilder])
(import [org.freedesktop.dbus.interfaces Properties])
connecting to the system bus:
(def sys-bus (DBusConnectionBuilder/forSystemBus))
(def sys-connection (.build sys-bus))
Next is making the call to Get
by creating a local proxy object that
connects to the remote object, and then simply invoking the method:
(def timedate1 (.getRemoteObject sys-connection
"org.freedesktop.timedate1"
"/org/freedesktop/timedate1"
Properties))
(.Get timedate1 "org.freedesktop.timedate1" "Timezone")
; => "America/New_York"
Not so bad, right? But wait, what’s up with that Properties
in the
.getRemoteObject
call? It’s the object’s interface class constructed as a
Java interface. I did not expect to need Java interfaces to make the call
since dbus-send and d-feet only needed the parameter list.
Thankfully dbus-java provides that interface, otherwise it would have to be built.
》KWallet
I’ll spare a ton of bits describing what happened next and just say that I decided to go with KWallet instead of the freedesktop.org secret service. There are quite a few dead and outdated links about the relationship between the two, and even now I’m not quite sure where it stands. Either should work, but I use KDE as my desktop so that’s the interface I will use going forward.
Exploring d-feet
shows the following:
service:
org.kde.kwalletd5
object path:
/modules/kwalletd5
interface:
org.kde.KWallet
》Getting the kwalletd5 Object
This part should be familiar. WKallet’s service is connected to the session bus, whereas the earlier example connected the system bus.
(def bus (DBusConnectionBuilder/forSystemBus))
(def connection (.build bus))
Like before, the remote object has to be connected, but what to use for the interface class?
(def kw (.getRemoteObject connection "org.kde.kwalletd5"
"/modules/kwalletd5" ...what?...))
The /modules/kwalletd5
object implements Introspectable
, Peer
, and Properties
interfaces from org.freedesktop.DBus
, bit it also has it’s own
org.kde.KWallet
interface which dbus-java does not provide!
Introspectable
will return an XML document describing the interface for the
KWallet object, and perhaps that could be used with Clojure’s gen-interface
and reify
to make the Java interface? I considered that but did not explore
it.
The dbus-java project describes another way to generate the interface code.
I’m very grateful that the kdewallet library for Java provides the necessary code so I didn’t have to generate it.
The Leiningen coordinates are [org.purejava/kdewallet "1.6.0"]
if you want to
follow along from here. Now to try getting the remote object again:
(import [org.purejava.kwallet KWallet])
(def kw (.getRemoteObject connection "org.kde.kwalletd5"
"/modules/kwalletd5" KWallet))
》The Rest is Interop - Or How to Use the Wallet
Now that the object is available, calls to the service are just normal Java interop. Either d-feet or busctl will show you the full list of methods.
What I found most useful was the test suite to guide how to use the service.
In short, the open
call ensures the wallet is open and returns a handle to it.
From there on out, most methods require both the handle and an identifier for
your client application.
The code below checks that the KWallet folder named "Nextcloud" exists, writes a test password/token and then reads it back.
;First, try to open the default wallet and get a handle for my app
(def kw-handle (.open kw "kdewallet" 0 "deck-cli"))
;Check for the folder (as show in the KDE Wallet Manager)
(.hasFolder kw kw-handle "Nextcloud" "deck-cli")
;Or create it
;(.createFolder kw kw-handle "Nextcloud" "deck-cli"))
;Get a list of entries in the folder
(.entryList kw kw-handle "Nextcloud" "deck-cli")
;Store a password
(.writePassword kw kw-handle "Nextcloud"
"deck-cli-token" "my-very-long-token" "deck-cli")
(.readPassword kw kw-handle "Nextcloud"
"deck-clj-token" "deck-cli") ;=> "my-very-long-token"
》Conclusion
Perhaps in the future I’ll look into how to handle signals from D-Bus, or even how to provide a service through it. But for now this suffices for my needs. Happy coding!