10 年前の [2016-05-06-2]
で、my-lisp-load という関数を紹介しました。アクセストークンなどを ~/.emacs.d/spec/ 配下のファイルに書いておき、init.el からはファイル名で読み込む関数です。
当時の目的は、init.el を GitHub に公開しても credential が漏れないようにすることでした。spec/ は git 管理外なので、この目的は今でも果たせています。
ただ、「git 管理外である」ことと「ストレージにない」ことは別の話です。spec/ にはトークンが平文のまま保存されているため、~/.emacs.d/ を丸ごと持っていくようなマルウェアには無力です。
2026 年の対策としては心もとないので、macOS の Keychain に移行しました。
なお以下は macOS 前提です。Linux なら secret-tool などに読み替えてください。
auth-source.el で Keychain から読むようにした
作ったのがこちらの my-secret-load 関数になります。
(defun my-secret-load (name)
"Load secret of NAME from macOS Keychain via auth-source
Register it beforehand as an internet password, since auth-source in
Emacs 31.1 cannot find generic passwords. Putting -w last prompts for
the value, so it never appears in argv. Add -U to update an existing
item.
$ security add-internet-password -s NAME -a emacs -w"
(require 'auth-source)
(let ((auth-sources '(macos-keychain-internet))
(auth-source-do-cache nil))
(or (auth-source-pick-first-password :host name :user "emacs")
(error "Cannot read %s from Keychain" name))))
my-secret-load は、Emacs 標準の auth-source.el にある auth-source-pick-first-password の薄いラッパーです。検索先を macOS Keychain の internet password に限定し、見つからなければエラーにしています。
auth-source.el はデフォルトで検索結果を 2 時間キャッシュします。ただ、見つからなかった結果もキャッシュされるため、Keychain に登録する前に一度呼んでいると、登録後もしばらく取得できません。そのため、キャッシュは無効にしています。
credential を読み込んでいた箇所は、my-lisp-load から my-secret-load に置き換えます。
;; before
(setq foo-access-token (my-lisp-load "foo-access-token"))
;; after
(setq foo-access-token (my-secret-load "foo-access-token"))
Keychain には事前にこのように登録しておきます。
$ security add-internet-password -s foo-access-token -a emacs -w
-a はアカウント名で、emacs に固定しています。my-secret-load の中で :user "emacs" として渡しているので、呼び出し側は名前だけを渡せば済みます。
-w を最後に指定すると、値を対話的に入力できます。-w "値" とは指定しないほうがよいです。~/.zsh_history などのシェル履歴に平文で残ってしまうためです。
保管先を Keychain にしたのは、私のパスワードマネージャが個人マシンは 1Password、業務マシンは Keeper と揃っていないためです。Keychain ならどちらのマシンでも同じ仕組みで済みます。
auth-source.el は Keychain 以外の保存先にも対応していますが、次の理由で採用しませんでした。
~/.authinfo.gpg- 復号時にパスフレーズを求められる
- Emacs の起動時に読み込むアクセストークンがあるため、gpg-agent のキャッシュが切れていると、Emacs を起動するたびにパスフレーズの入力が必要になる
- pass CLI(auth-source-pass.el)
- 中身は GPG なので、パスフレーズの手間は
~/.authinfo.gpgと同じ - 別途 pass CLI のインストールも必要になる
- 中身は GPG なので、パスフレーズの手間は
- 1Password(auth-source-1password.el など)
- サードパーティのパッケージと 1Password CLI が必要になる
- 前述のとおり、業務マシンは Keeper なので揃わない
Keychain は macOS へのログイン時にロックが解除されるので、Emacs の起動時に何も入力せずに済みます。
また、Keychain にアクセスしているのは Emacs.app 自身ではなく、auth-source.el が呼び出す security CLI です。Keychain のアクセス許可はアプリごとに記録されるので、Emacs.app を更新しても、改めて許可を求められることはありません。
なお my-lisp-load は引き続き利用しています。user-mail-address などは credential ではないものの、個人マシンと業務マシンで値が異なり、あまり公開したくもないので、spec/ に残しています。
generic password は Emacs 31.1 では取得できない
Keychain のパスワードには、generic password と internet password の 2 種類があります。auth-source.el はどちらにも対応しています。アプリの設定値を入れるなら generic password の方が素直ですが、Emacs 31.1 では取得できませんでした。
(let ((auth-sources '(macos-keychain-generic))
(auth-source-do-cache nil))
(auth-source-pick-first-password :port "foo-access-token" :user "emacs"))
;; => nil
macos-keychain-generic では、security CLI の -s(サービス名)に渡したい値を :host ではなく :port に載せます。分かりにくい対応ですが、上のコードはこれに従っています。それでも nil が返ります。
:type が途中で取り除かれるため
lisp/auth-source.el
を確認したところ、原因が分かりました。
auth-source-search は、バックエンドの search 関数に :type を付けて渡します。受け取った auth-source-macos-keychain-search は、実処理の auth-source-macos-keychain-search-items へ渡す前に、auth-source-search-spec
で spec のキーを絞ります。このとき除外されるのが auth-source-ignored-keys
です。
(defconst auth-source-ignored-keys
'(:create :delete :max :backend :label :require :type)
"List of meta keys to be ignored in data stores.")
:type が入っているので、ここで spec から取り除かれます。
一方、search-items 側
はキーワード引数の type を見て、generic かどうかを判定しています。
(cl-defun auth-source-macos-keychain-search-items (coll _type _max
host port user
&key label type
&allow-other-keys)
(let* ((keychain-generic (eq type 'macos-keychain-generic))
type は常に nil になるので、find-generic-password ではなく find-internet-password が実行されます。generic password が見つからないのは当然です。
実際、:type を明示して search-items を直接呼ぶと取得できました。
(auth-source-macos-keychain-search-items
"default" nil 1 nil "foo-access-token" "emacs"
:type 'macos-keychain-generic)
;; => ((:type macos-keychain-generic :secret ... ))
おそらく 31.1 でのエンバグ
Emacs 30.2 では、同じ generic password を auth-source.el で取得できました。30.2 までは、keychain バックエンドが自前で除外キーのリストを持っていて、:type はありませんでした。
;; Filter out ignored keys from the spec
(ignored-keys '(:create :delete :max :backend :label :host :port))
除外キーを絞るループは 3 箇所にコピペされていて、keychain バックエンドのものには FIXME コメントが付いていました。リストの中身はバックエンドごとに違い、他の 2 箇所には :type が含まれていました。それを a142cc26
(2025-10-05)で共通化した際に、keychain バックエンドにも :type が紛れ込んだようです。
コミットメッセージにも 31.1 の NEWS
にも、generic password の扱いを変えたという記載はありません。auth-source-macos-keychain-search の docstring にも macos-keychain-generic の使用例が残っているので、意図した仕様変更とは考えにくく、おそらくエンバグだと思います。
macos-keychain-internet の方は、type が nil でも find-internet-password が実行されるので、結果として正しく取得できます。壊れているのは generic だけです。片方にしか影響が出なかったのも、見逃された一因だと思います。
debbugs を件名で検索した限りでは、報告は見当たりませんでした。一旦報告はせず、Emacs 側で直ったら追随するつもりです。
ということで、internet password として登録することにしました。
Keychain でも防げないもの
Keychain に移行して防げるのは、~/.emacs.d/ のファイルを持ち出すだけの相手までです。
同じユーザー権限でコマンドを実行できる相手は防げません。security find-internet-password -s foo-access-token -w を叩けば取れてしまいます。
Keychain にはアプリごとのアクセス制御もありますが、許可されているのは security CLI だけです。

そのため、Emacs から呼んだのか、シェルから呼んだのかは区別できません。
こうなると有効なのは、Touch ID のように毎回人間が介在する仕組みか、トークン自体の権限を絞り、有効期限を短くしておくことくらいです。盗まれる前提で、被害を小さくしておくのが現実的だと思います。
まとめ
~/.emacs.d/spec/ の平文ファイルに保存していた credential を、macOS Keychain に移行しました。
2016(my-lisp-load) |
2026(my-secret-load) |
|
|---|---|---|
| 守る相手 | GitHub に公開したときの流出 | ファイルを持ち出すマルウェア |
| やったこと | init.el から値を追い出し、git 管理外のファイルに保存する | ストレージに平文で保存するのをやめ、Keychain に移行する |
| 残った課題 | ストレージ上は平文のまま | macOS 縛り。同じユーザー権限で security CLI を叩ける相手には防げない |
10 年越しに、意図せず伏線を回収した形です。途中で Emacs 31.1 のエンバグらしきものにも遭遇しました。
Keychain に移行しても、同じユーザー権限でコマンドを実行できる相手には読まれます。トークンを Keychain に隠すことと、権限を絞り有効期限を短くすることを組み合わせた、多重防御が大事だと思います。
どなたかの参考になれば幸いです。