大部分 AI 相关的 Emacs 包都推荐用 auth-source
来存取用到 API Key。不过除了 auth-source
外,Emacs 还内置了一个 secrets
包,可以和 org.freedesktop.Secrets
通信来管理 secret。
在 Linux 桌面环境上用得比较多的 Secrets API 实现应该是 KDE Wallet 和 GNOME Keyring,但我一直都没搞懂 KDE Wallet 是怎么工作的,所以我用的是 KeePassXC 的实现:
落实在 Emacs 中就是用 secrets-create-item
创建新的 API key 和用 secrets-get-secret
获取已经保存的 API key。
这里是一个给 gptel
接 API key 的例子:
(setq gptel-backend
(gptel-make-openai "OpenRouter"
:host "openrouter.ai"
:endpoint "/api/v1/chat/completions"
:stream t
:key (lambda ()
(secrets-get-secret "Main" "emacs-openrouter-api-key"))
:models '(google/gemini-2.5-pro-preview)))
至于具体怎么使用 KeePassXC 作为 Emacs 的钥匙圈,有兴趣的话可以看一下我博客上一篇文章。
7 个赞
douo
2
把 key 放到环境变量,Linux 一个 grep /proc/*/environ
就全出来了。
你应该是指 Aidermacs 设参数传 API key 的方式?Aider 似乎也可以用命令行参数的形式传进去,但感觉都是权宜之策,我打算给 Aider 提个 PR 什么的,但等做出来合并之前好像也只能这样凑合 
刚看了下 secrets.el 的代码,发现 secrets.el 只支持 plain 算法,不支持 dh-ietf1024-sha256-aes128-cbc-pkcs7,这样的话 D-Bus 传输的密码也是没有加密的。
Hack 了一下,加了 dh-ietf1024-sha256-aes128-cbc-pkcs7
的支持。
From f5cbc9abd5acdc0637480817cb2d037e816ed273 Mon Sep 17 00:00:00 2001
From: Zhengyi Fu <[email protected]>
Date: Sat, 14 Jun 2025 00:17:23 +0800
Subject: [PATCH] Add support for dh-ietf1024-sha256-aes128-cbc-pkcs7 to
secrets.el
* lisp/net/secrets.el (secrets-session-algorithm)
(secrets-session-aes-key, secrets-dh-prime): New variables.
(secrets-integer-to-unibyte-string)
(secret-unibyte-string-to-integer, secrets-generate-key-pair)
(secrets-compute-aes-key, secrets-unibyte-string-to-byte-array):
New functions.
(secrets-close-session): Reset `secrets-session-aes-key'.
(secrets-open-session): Use `dh-ietf1024-sha256-aes128-cbc-pkcs7'
algorithm first. If failed, then fallback to `plain'.
(secrets-pkcs7-pad, secrets-pkcs7-unpad, secrets-encrypt-secret)
(secrets-decrypt-secret): New functions.
(secrets-create-item, secrets-get-secret): Use
`secrets-encrypt-secret' and `secrets-decrypt-secret'
---
lisp/net/secrets.el | 149 ++++++++++++++++++++++++++++++++++++++++----
1 file changed, 136 insertions(+), 13 deletions(-)
diff --git a/lisp/net/secrets.el b/lisp/net/secrets.el
index 049def1dc8d..8170d089639 100644
--- a/lisp/net/secrets.el
+++ b/lisp/net/secrets.el
@@ -329,13 +329,73 @@ secrets-session-path
"The D-Bus session path of the active session.
A session path `secrets-empty-path' indicates there is no open session.")
+(defvar secrets-session-algorithm "dh-ietf1024-sha256-aes128-cbc-pkcs7")
+
+(defvar secrets-session-aes-key nil)
+
+(defconst secrets-dh-prime #xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF)
+
+(defun secrets-integer-to-unibyte-string (integer length endian)
+ "Convert INTEGER to a unibyte string with LENGTH bytes.
+ENDIAN is `big' or `little'."
+ (let ((str (make-string length 0)))
+ (dotimes (i length)
+ (let ((n (mod integer 256)))
+ (setq integer (/ integer 256))
+ (setf (aref str i) n)))
+ (if (eq endian 'big)
+ (reverse str)
+ str)))
+
+(defun secret-unibyte-string-to-integer (str endian)
+ "Convert STR to an integer.
+ENDIAN is `big' or `little'."
+ (if (eq endian 'little)
+ (setq str (reverse str)))
+ (let ((num 0)
+ (len (length str)))
+ (dotimes (i len)
+ (setq num (+ (* num 256) (aref str i))))
+ num))
+
+(defun secrets-generate-key-pair ()
+ "Generate a key pair with Diffie-Hellman key agreement using `secrets-dh-prime'.
+Return a list like (PRIVATE-KEY PUBLIC-KEY)."
+ (require 'calc-math)
+ (let* ((calc-display-working-message nil)
+ (priv-key (random (ash 1 1024)))
+ (pub-key (math-pow-mod 2 priv-key secrets-dh-prime)))
+ (list
+ (secrets-integer-to-unibyte-string priv-key 128 'big)
+ (secrets-integer-to-unibyte-string pub-key 128 'big))))
+
+(defun secrets-compute-aes-key (priv-key peer-pub-key)
+ "Compute the symmetric key used for AES encryption."
+ (setq priv-key (secret-unibyte-string-to-integer priv-key 'big))
+ (setq peer-pub-key (secret-unibyte-string-to-integer peer-pub-key 'big))
+ (let* ((calc-display-working-message nil)
+ (shared-secret (math-pow-mod peer-pub-key priv-key secrets-dh-prime))
+ (shared-secret (secrets-integer-to-unibyte-string shared-secret 128 'big))
+ (salt (make-string 32 0))
+ (shared-key (gnutls-hash-mac "SHA256" salt shared-secret))
+ (aes-key (substring (gnutls-hash-mac "SHA256" shared-key "") 0 16)))
+ aes-key))
+
+(defun secrets-unibyte-string-to-byte-array (ustr)
+ "Convert unibyte string USTR to a D-Bus byte array."
+ (cons :array
+ (mapcan (lambda (byte)
+ (list :byte byte))
+ ustr)))
+
(defun secrets-close-session ()
"Close the secret service session, if any."
(dbus-ignore-errors
(dbus-call-method
:session secrets-service secrets-session-path
secrets-interface-session "Close"))
- (setq secrets-session-path secrets-empty-path))
+ (setq secrets-session-path secrets-empty-path
+ secrets-session-aes-key nil))
(defun secrets-open-session (&optional reopen)
"Open a new session with \"plain\" algorithm.
@@ -344,15 +404,78 @@ secrets-open-session
returned, and it will be stored in `secrets-session-path'."
(when reopen (secrets-close-session))
(when (secrets-empty-path secrets-session-path)
- (setq secrets-session-path
- (cadr
- (dbus-call-method
- :session secrets-service secrets-path
- secrets-interface-service "OpenSession" "plain" '(:variant "")))))
+ (let* ((key-pair (secrets-generate-key-pair))
+ (pub-key (secrets-unibyte-string-to-byte-array (cadr key-pair)))
+ (results
+ (condition-case err
+ (dbus-call-method
+ :session secrets-service secrets-path
+ secrets-interface-service "OpenSession"
+ secrets-session-algorithm
+ `(:variant ,pub-key))
+ (dbus-error
+ (unless (equal (cadr err) "org.freedesktop.DBus.Error.NotSupported")
+ (signal (car err) (cdr err)))
+ (when (equal secrets-session-algorithm "plain")
+ (signal (car err) (cdr err)))
+ (setq secrets-session-algorithm "plain")
+ (dbus-call-method
+ :session secrets-service secrets-path
+ secrets-interface-service "OpenSession"
+ secrets-session-algorithm
+ `(:variant "")))))
+ (peer-pub-key (apply #'unibyte-string (caar results))))
+ (when secrets-session-algorithm
+ (setq secrets-session-aes-key
+ (secrets-compute-aes-key (car key-pair) peer-pub-key)))
+ (setq secrets-session-path (cadr results))))
(when secrets-debug
(message "Secret Service session: %s" secrets-session-path))
secrets-session-path)
+(defun secrets-pkcs7-pad (data)
+ "Pad DATA to 128 bits."
+ (let ((padding (- 16 (mod (length data) 16))))
+ (concat data (make-string padding padding))))
+
+(defun secrets-pkcs7-unpad (data)
+ "Remove padding bytes from DATA."
+ (let ((padding (aref data (1- (length data)))))
+ (if (<= padding 16)
+ (substring data 0 (- (length data) padding))
+ data)))
+
+(defun secrets-encrypt-secret (password)
+ "Encrypt PASSWORD. Return (IV ENCRYPTED-PASSWORD)."
+ (setq password (encode-coding-string password 'binary))
+ (pcase secrets-session-algorithm
+ ("dh-ietf1024-sha256-aes128-cbc-pkcs7"
+ (let* ((iv (random (ash 1 128)))
+ (iv (secrets-integer-to-unibyte-string iv))
+ (encrypted (gnutls-symmetric-encrypt
+ "AES-128-CBC"
+ (substring secrets-session-aes-key)
+ (substring iv)
+ (secrets-pkcs7-pad password))))
+ (reverse encrypted)))
+ ("plain"
+ (list "" password))))
+
+(defun secrets-decrypt-secret (iv encrypted-value)
+ "Decrypt ENCRYPTED-VALUE."
+ (let ((password
+ (pcase secrets-session-algorithm
+ ("dh-ietf1024-sha256-aes128-cbc-pkcs7"
+ (secrets-pkcs7-unpad
+ (car
+ (gnutls-symmetric-decrypt "AES-128-CBC"
+ (substring secrets-session-aes-key)
+ (substring iv)
+ (substring encrypted-value)))))
+ ("plain"
+ encrypted-value))))
+ (decode-coding-string password 'utf-8)))
+
;;; Prompts.
(defvar secrets-prompt-signal nil
@@ -664,8 +787,7 @@ secrets-create-item
(:variant ,(append '(:array) props))))))
;; Secret.
`(:struct :object-path ,secrets-session-path
- (:array :signature "y") ;; No parameters.
- ,(dbus-string-to-byte-array password)
+ ,@(secret-encrypt-secret password)
,secrets-struct-secret-content-type)
;; Do not replace. Replace does not seem to work.
nil))
@@ -720,11 +842,12 @@ secrets-get-secret
ITEM can also be an object path, which is used if contained in COLLECTION."
(let ((item-path (secrets-unlock-item collection item)))
(unless (secrets-empty-path item-path)
- (dbus-byte-array-to-string
- (nth 2
- (dbus-call-method
- :session secrets-service item-path secrets-interface-item
- "GetSecret" :object-path secrets-session-path))))))
+ (let* ((secret (dbus-call-method
+ :session secrets-service item-path secrets-interface-item
+ "GetSecret" :object-path secrets-session-path)))
+ (secrets-decrypt-secret
+ (apply #'unibyte-string (nth 1 secret))
+ (apply #'unibyte-string (nth 2 secret)))))))
(defun secrets-get-attributes (collection item)
"Return the lookup attributes of item labeled ITEM in COLLECTION.