age.el is intended to provide transparent age[1] based file encryption
and decryption in Emacs. As such age.el does not support all
age CLI based use cases. Rather age.el assumes you have configured
a default identity and a default recipient, e.g. based off your
ssh private key and ssh public key in .ssh/id_rsa[.pub]
, which
is the default setting.
The main use case is for folks who like to e.g. encrypt their Org
notes and things of that nature. Since age.el provides a direct
port of EPG/EPA functionality it can support all roles that .gpg
files can support in Emacs, e.g. .authinfo.age
should work fine as well.
[1]: https://github.com/FiloSottile/age
Put age.el somewhere in your load-path and:
(require 'age)
(age-file-enable)
If you prefer to use use-package
, a quelpa configuration looks like:
(use-package age
:quelpa (age :fetcher github :repo "anticomputer/age.el")
:ensure t
:demand
:config
(age-file-enable))
You can now open, edit, and save age encryted files from Emacs as long as they contain the .age extension.
age.el also supports creating new .age files through find-file and they will be encrypted to your default recipient on first save.
Example:
M-x find-file RET /tmp/test.age RET M-x save-buffer RET
Will create an age encrypted file named test.age in the /tmp
directory. It will be encrypted to a recipient of .ssh/id_rsa.pub
by default and decrypted with an identity of .ssh/id_rsa
by default.
You can customize the default key values via age-default-recipient
and
age-default-identity
, respectively.
age.el tries to remain composable with the core philosophy of age itself and as such does not try to provide a kitchen sink worth of features.
You can find my current configuration for age.el below. I am using age-yubikey-plugin to supply an age identity off of a yubikey PIV slot. The slot is configured to require a touch (with a 15 second cache) for every age client query against the identity stored in that slot.
This means that every age.el decrypt requires a physical touch for confirmation. The cache makes it such that e.g. decrypting a series of age encrypted org files in sequence only requires a single touch confirmation.
This limits the amount of actively accessible encrypted data inside Emacs to only the things I physically confirm, and only for 15 second windows, but without having to type a passphrase at any point. This excludes any open buffers that have decrypted data in memory of course.
The key scheme I employ encrypts against the public keys of two main identities. My aforementioned yubikey identity as well as a disaster recovery identity, who’s private key is passphrase encrypted and kept in cold storage.
You’ll note that I’ve set age-default-identity
and age-default-recipient
to be lists. These two variables can be file paths, key strings, or lists that
contain a mix of both. This allows you to easily encrypt to a series of
identities in whatever way you choose to store and manage them.
Note that I’m using rage as opposed to age as my age client. This is due the aforementioned lack of pinentry support in the reference age implemention, which rage does support.
(use-package age
:quelpa (age :fetcher github :repo "anticomputer/age.el")
:ensure t
:demand
:custom
;; use rage for pinentry, note this _has_ to go through customize
(age-program "rage")
(age-default-identity "~/.ssh/age_yubikey")
(age-default-recipient
'("~/.ssh/age_yubikey.pub"
"~/.ssh/age_recovery.pub"))
:config
(age-file-enable))
(provide 'my-age-init)
I use the above configuration in combination with a version of org-roam
that
has the following patches applied:
https://patch-diff.githubusercontent.com/raw/org-roam/org-roam/pull/2302.patch
This patch enables .org.age
discoverability in org-roam
and beyond that
everything just works the same as you’re used to with .org.gpg
files. This
patch was merged into org-roam main
on Dec 31, 2022, so any org-roam release
post that date should provide you with age support out of the box.
(defun my/age-github-keys-for (username)
"Turn GitHub USERNAME into a list of ssh public keys."
(let* ((res (shell-command-to-string
(format "curl -s https://api.github.com/users/%s/keys"
(shell-quote-argument username))))
(json (json-parse-string res :object-type 'alist)))
(cl-assert (arrayp json))
(cl-loop for alist across json
for key = (cdr (assoc 'key alist))
when (and (stringp key)
(string-match-p "^\\(ssh-rsa\\|ssh-ed25519\\) AAAA" key))
collect key)))
(defun my/age-save-with-github-recipient (username)
"Encrypt an age file to the public keys of GitHub USERNAME."
(interactive "MGitHub username: ")
(cl-letf (((symbol-value 'age-default-recipient)
(append (if (listp age-default-recipient)
age-default-recipient
(list age-default-recipient))
(my/age-github-keys-for username))))
(save-buffer)))
Since I use a yubikey touch controlled age identity I find it useful to have a
visual indication of when age.el is performing operations that might require
me to touch the yubikey. The following advice adds visual notifications to
age-start-decrypt
and age-start-encrypt
.
I’m also using this as a way to get a good feel for just how much Emacs is interacting with my encrypted data.
(require 'notifications)
(defun my/age-notify (msg)
(cond ((eq system-type 'gnu/linux)
(notifications-notify
:title "age.el"
:body (format "%s" msg)
:urgency 'low
:timeout 800))
((eq system-type 'darwin)
(do-applescript
(format "display notification \"%s\" with title \"age.el\"" msg)))
(t
(message (format "age.el: %s" msg)))))
(defun my/age-notify-decrypt (&rest args)
(my/age-notify "decrypt"))
(defun my/age-notify-encrypt (&rest args)
(my/age-notify "encrypt"))
(defun my/age-toggle-decrypt-notifications ()
(interactive)
(cond ((advice-member-p #'my/age-notify-decrypt #'age-start-decrypt)
(advice-remove #'age-start-decrypt #'my/age-notify-decrypt)
(message "Disabled age decrypt notifications."))
(t
(advice-add #'age-start-decrypt :before #'my/age-notify-decrypt)
(message "Enabled age decrypt notifications."))))
(defun my/age-toggle-encrypt-notifications ()
(interactive)
(cond ((advice-member-p #'my/age-notify-encrypt #'age-start-encrypt)
(advice-remove #'age-start-encrypt #'my/age-notify-encrypt)
(message "Disabled age encrypt notifications."))
(t
(advice-add #'age-start-encrypt :before #'my/age-notify-encrypt)
(message "Enabled age encrypt notifications."))))
;; we only care about decrypt notifications really
(my/age-toggle-decrypt-notifications)
The age reference client does not support pinentry by design. Users are encouraged to use identity (private) keys and recipient (public) keys, and manage those secrets outside of Emacs accordingly. As such age.el does not currently support passphrase based age Encryption/Decryption as we do not have a tty available to provide a passphrase to age (I think).
You can work around this by using rage instead of age, which is a Rust based implementation of the Age spec and which does support pinentry by default and age.el will work with rage as well. An example rage config may look like:
(use-package age
:quelpa (age :fetcher github :repo "anticomputer/age.el")
:ensure t
:demand
:custom
;; use rage for pinentry, note this _has_ to go through customize
(age-program "rage")
(age-default-identity "~/.ssh/id_rsa")
(age-default-recipient "~/.ssh/id_rsa.pub")
:config
(age-file-enable))
You will now be able to use passphrase protected ssh keys as well:
If you’d like to keep your pinentry support inside of emacs entirely for
whatever reason, you can use pinentry-emacs
for a pinentry program that
will prompt you inside of Emacs. Most distributions have a package for
pinentry-emacs
available, which provides a GNU pinentry executable with the
Emacs flavor enabled.
If your distribution does not provide an Emacs enabled build of GNU pinentry, you can find the GNU pinentry collection, which contains the Emacs flavor of pinentry as well here.
Warning: don’t confuse GNU pinentry with this pinentry-emacs shellscript they are not the same thing.
Note: if you’re saying file not found
errors when trying to use pinentry
you’ll also want to ensure the Emacs pinentry socket actually exists and is
running by using the GNU ELPA pinentry package:
(use-package pinentry
:config
(pinentry-start))
With both of those requirements satisfied, rage will use pinentry-emacs
to
prompt you for passphrases in the minibuffer.
This requires you to use rage, or another age-spec compliant client that supports pinentry.
By default, age.el will be able to open and save passphrase encrypted age files. It will detect the scrypt stanza in the age file and set the age.el handling context for passphrase mode accordingly.
You can also programmatically force age.el into passphrase mode by binding
age-default-identity
and age-default-recipient
to nil temporarily, e.g.:
(defun my/age-open-with-passphrase (file)
(interactive "fPassphrase encrypted age file: ")
(cl-letf (((symbol-value 'age-default-identity) nil)
((symbol-value 'age-default-recipient) nil))
(find-file file)))
(defun my/age-save-with-passphrase ()
(interactive)
(cl-letf (((symbol-value 'age-default-identity) nil)
((symbol-value 'age-default-recipient) nil))
(save-buffer)))
Org-roam has merged org-roam/org-roam#2302 which
provides .org.age
discoverability support for org-roam, so if you update to
the latest release from e.g. MELPA or the main branch, org-roam will function
with .age encrypted org files.
This is experimental software and subject to heavy feature iterations.
This is, apparently, a heated topic and folks more qualified than me have commented on this in great detail over many years. The following blog posts I think provide a good summary of the state of the debate regarding the OpenPGP specification:
Thanks to reddit’s /u/a-huge-waste-of-time
for linking those references.
In true megalomaniac fashion I’ll quote myself out of the age.el /r/emacs
announcement thread when asked why I was looking to rid myself of gpg:
I wanted to reduce the amount of key management in my life to the bare minimum. I don’t use gpg for its intended purpose (maintaining a web of trust with folks that you communicate with), but rather only use it for Emacs file encryption and things like password-store (which I’m replacing with https://github.com/FiloSottile/passage and will also port the Emacs pass frontend to work with).
Age functions with ssh keys as well as its own key formats, so it hugely simplifies the amount of key material I have to maintain. Especially when managing key material on e.g. YubiKeys, maintaining Encryption, Authentication, and Signing subkeys and juggling what is essentially a personal PKI (not to mention bringing it along on every system) surrounding gpg’s key trust relationship maintainance.
I use e2e encrypted email and messaging services for encrypted communications and ssh keys to sign git commits.
So with age I can also just use my ssh public key to encrypt and my ssh private key to decrypt my files. If I want to get fancy, I can use something like https://github.com/str4d/age-plugin-yubikey to provide the key material for my age operations (which should compose with age.el quite well also, i.e. you can have every decrypt operation have a touch requirement in Emacs that way).
TL;DR: gpg is overly complex for my use case and I’m currently shoehorning gpg into a role it was never designed or intended to play. Complexity of use and secure use of cryptography don’t compose well for most folks, so now that gpg no longer serves any real purpose in my environment, it’s time to retire it from my dependency stack.
Having said that, age.el is not intended to encourage you to abandon gpg. However, if you’ve been looking for a lighter weight alternative for Emacs encryption, it might be a good fit for you.
This code was ported from the existing EPA and EPG Emacs code and as such their original copyright applies:
Copyright (C) 1999-2000, 2002-2022 Free Software Foundation, Inc.
Author: Daiki Ueno <ueno@unixuser.org> Keywords: emacs Version: 1.0.0
This file is part of GNU Emacs.
GNU Emacs is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
GNU Emacs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with GNU Emacs. If not, see https://www.gnu.org/licenses/.