QubesOS in Emacs Org Mode: Link to files of other qubes
Table of Contents
1. Introduction
I use Emacs Org Mode to maintain a personal wiki for pretty much everything. It includes my appointments and todos, university lecture notes, flash cards for spaced repetition learning, diary entries and more. Since much of the information is quite sensitive and since all of the pages are created by myself, they reside in a network-isolated, highly trusted qube.
Sometimes, though, I want to link to a file residing in another qube (for example, an untrusted PDF of an academic article residing in my work qube. When I click the link, it should open the file in the corresponding qube. Additionally, I want auto completion of the file path when creating the link. This blog post details how I achieved both of these requirements.
2. Qrexec services
2.1. Opening files of a qube
Surprisingly, qubes does not ship with a qrexec service with which qube A can
instruct qube B to open one of qube B's files. qvm-open-in-vm
does something
slightly different: It enables you to copy a file from qube A to qube B and then
open it in qube B.
qvm-open-in-vm
also allows opening a URL in the target qube. My initial
thought was: "Great, I can just pass a file://
URL to qvm-open-in-vm
(for
example, file:///home/user/Documents/article.pdf
) and that should lead to the
target qube opening the corresponding file". This almost works, but
qvm-open-in-vm
restricts the types of URLs that can be used with it, and
file://
URLs aren't one of them. So, we need to copy and slightly modify the
service.
First, in my template qube, I created a copy of /etc/qubes-rpc/qubes.OpenURL
to /etc/qubes-rpc/my.OpenURL
. The original lists three different allowed URL
schemes (http://
, https://
, ftp://
). These simply needed to be extended
with the additional file://
scheme. The result looks like this.
#!/usr/bin/sh read -r url case "$url" in http://*|\ https://*|\ ftp://*|\ file://*) exec qubes-open "$url" ;; *) echo "Invalid URL" >&2 exit 1 ;; esac
Next, still in my template qube, I created a copy of /usr/bin/qvm-open-in-vm
to /usr/bin/my-open-in-vm
. In that program, I simply needed to replace every
occurrence of qubes.OpenURL
with my.OpenURL
and every occurrence of
qvm-open-in-vm
with my-open-in-vm
.
Finally, I created a policy file in dom0
, at
/etc/qubes-rpc/policy/my.OpenURL
. Mine looks like this.
personal work allow @anyvm @anyvm ask
This means that my personal qube is automatically allowed to call the service on my work qube. Any qube can ask for permission to call the service on any other qube.
After shutting down your template qube and restarting your qubes that depend on it, you can test the service by running
$ my-open-in-vm targetvm file:///path/to/file
in a qube of your choice.
2.2. Autocompletion of file paths
The following qrexec service returns a list of possible auto completions for a given path. It consists of the following parts.
/etc/qubes-rpc/my.Ls
:
#!/bin/bash read args output="$(ls -d -p $args*)" status="$?" echo "$status" echo "$output"
/usr/bin/my-ls-client
:
#!/bin/bash echo $@ read status if [ "$status" -ne 0 ]; then exit $status else exec cat >&$SAVED_FD_1 fi
/usr/bin/my-ls
:
#!/bin/bash qube="$1" args=${@:2} qrexec-client-vm "$qube" my.Ls /usr/bin/my-ls-client $args
You can test the service by running
$ my-ls targetvm /path/to/compl
in a qube of your choice. Don't forget that you need to create a qrexec policy
in dom0
, as described in the previous section.
3. Emacs Configuration
Finally, we need to teach Emacs to use these qrexec services. The following function simply calls the completion qrexec service.
(defun my-qubes-list-files (qube &optional dir) (condition-case nil (process-lines "/usr/bin/my-ls" qube dir) (error nil)))
Next, I created a completion function as required by completing-read
(see the
Emacs lisp documentation on completion in minibuffers if you're interested).
(defun my-qubes-completion-function (qube str &optional predicate flag) (let ((files (or (my-qubes-list-files qube str) (my-qubes-list-files qube (replace-regexp-in-string "[^/]*$" "" str))))) (cond ((eq flag nil) (try-completion str files predicate)) ((eq flag t) (all-completions str files predicate)) ((eq flag 'lambda) (test-completion str files predicate)) ((eq flag 'metadata) '(metadata . nil)) ((listp flag) (completion-boundaries str files predicate (cdr flag))) nil)))
Next, org-link-parameters
needs a function that takes care of calling
completing-read
(obtaining the input from the user) and returning the link.
The following function first asks the user for the target qube and then prompts
them for the file, with autocompletion enabled.
(defun my-qubes-link-completion () (let ((qube (read-from-minibuffer "Qube: "))) (concat "qubes:" qube ":" (completing-read "File: " (lambda (str predicate flag) (my-qubes-completion-function qube str predicate flag)) nil nil "/home/user/"))))
Finally, org-link-parameters
also needs a function that specifies what happens
when following (i. e. clicking on) the link. This simply calls our
my-open-in-vm
service.
(defun my-qubes-follow-link (path _) (let ((args (split-string path ":")) (async-shell-command-buffer "rename-buffer") (display-buffer-alist '((t display-buffer-no-window)))) (async-shell-command (concat "my-open-in-vm " (car args) " file://" (cadr args)) "qubes")))
Now that I had everything I need, I could specify the new qubes
org link type.
(org-link-set-parameters "qubes" :follow #'my-qubes-follow-link :complete #'my-qubes-link-completion)
Links take the form qubes:targetvm:/path/to/file
. Everything works as
intended as far as I can tell. The only annoyance is that the qrexec service
for opening the file only terminates when you exit the program with which the
file is being viewed. Thus, upon trying to close Emacs, it may warn you that
there is still an active process running. If anyone knows a solution for this,
feel free to contact me and I will update this blog post accordingly.