summaryrefslogtreecommitdiff
path: root/lisp/net/pinentry.el
blob: 3e43b7d9dea9079b7d105a9d0b85f6ba03cfd20a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
;;; pinentry.el --- GnuPG Pinentry server implementation -*- lexical-binding: t -*-

;; Copyright (C) 2015-2017 Free Software Foundation, Inc.

;; Author: Daiki Ueno <ueno@gnu.org>
;; Version: 0.1
;; Keywords: GnuPG

;; 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 <http://www.gnu.org/licenses/>.

;;; Commentary:

;; This package allows GnuPG passphrase to be prompted through the
;; minibuffer instead of graphical dialog.
;;
;; To use, add "allow-emacs-pinentry" to "~/.gnupg/gpg-agent.conf",
;; reload the configuration with "gpgconf --reload gpg-agent", and
;; start the server with M-x pinentry-start.
;;
;; The actual communication path between the relevant components is
;; as follows:
;;
;;   gpg --> gpg-agent --> pinentry --> Emacs
;;
;; where pinentry and Emacs communicate through a Unix domain socket
;; created at:
;;
;;   ${TMPDIR-/tmp}/emacs$(id -u)/pinentry
;;
;; under the same directory which server.el uses.  The protocol is a
;; subset of the Pinentry Assuan protocol described in (info
;; "(pinentry) Protocol").
;;
;; NOTE: As of August 2015, this feature requires newer versions of
;; GnuPG (2.1.5+) and Pinentry (0.9.5+).

;;; Code:

(eval-when-compile (require 'cl-lib))

(defgroup pinentry nil
  "The Pinentry server"
  :version "25.1"
  :group 'external)

(defcustom pinentry-popup-prompt-window t
  "If non-nil, display multiline prompt in another window."
  :type 'boolean
  :group 'pinentry)

(defcustom pinentry-prompt-window-height 5
  "Number of lines used to display multiline prompt."
  :type 'integer
  :group 'pinentry)

(defvar pinentry-debug nil)
(defvar pinentry-debug-buffer nil)
(defvar pinentry--server-process nil)
(defvar pinentry--connection-process-list nil)

(defvar pinentry--labels nil)
(put 'pinentry-read-point 'permanent-local t)
(defvar pinentry--read-point nil)
(put 'pinentry--read-point 'permanent-local t)

(defvar pinentry--prompt-buffer nil)

;; We use the same location as `server-socket-dir', when local sockets
;; are supported.
(defvar pinentry--socket-dir
  (format "%s/emacs%d" (or (getenv "TMPDIR") "/tmp") (user-uid))
  "The directory in which to place the server socket.
If local sockets are not supported, this is nil.")

(defconst pinentry--set-label-commands
  '("SETPROMPT" "SETTITLE" "SETDESC"
    "SETREPEAT" "SETREPEATERROR"
    "SETOK" "SETCANCEL" "SETNOTOK"))

;; These error codes are defined in libgpg-error/src/err-codes.h.in.
(defmacro pinentry--error-code (code)
  (logior (lsh 5 24) code))
(defconst pinentry--error-not-implemented
  (cons (pinentry--error-code 69) "not implemented"))
(defconst pinentry--error-cancelled
  (cons (pinentry--error-code 99) "cancelled"))
(defconst pinentry--error-not-confirmed
  (cons (pinentry--error-code 114) "not confirmed"))

(autoload 'server-ensure-safe-dir "server")

(defvar pinentry-prompt-mode-map
  (let ((keymap (make-sparse-keymap)))
    (define-key keymap "q" 'quit-window)
    keymap))

(define-derived-mode pinentry-prompt-mode special-mode "Pinentry"
  "Major mode for `pinentry--prompt-buffer'."
  (buffer-disable-undo)
  (setq truncate-lines t
	buffer-read-only t))

(defun pinentry--prompt (labels query-function &rest query-args)
  (let ((desc (cdr (assq 'desc labels)))
        (error (cdr (assq 'error labels)))
        (prompt (cdr (assq 'prompt labels))))
    (when (string-match "[ \n]*\\'" prompt)
      (setq prompt (concat
                    (substring
                     prompt 0 (match-beginning 0)) " ")))
    (when error
      (setq desc (concat "Error: " (propertize error 'face 'error)
                         "\n" desc)))
    (if (and desc pinentry-popup-prompt-window)
      (save-window-excursion
        (delete-other-windows)
	(unless (and pinentry--prompt-buffer
                     (buffer-live-p pinentry--prompt-buffer))
	  (setq pinentry--prompt-buffer (generate-new-buffer "*Pinentry*")))
	(if (get-buffer-window pinentry--prompt-buffer)
	    (delete-window (get-buffer-window pinentry--prompt-buffer)))
	(with-current-buffer pinentry--prompt-buffer
	  (let ((inhibit-read-only t)
		buffer-read-only)
	    (erase-buffer)
	    (insert desc))
	  (pinentry-prompt-mode)
	  (goto-char (point-min)))
	(if (> (window-height)
	       pinentry-prompt-window-height)
	    (set-window-buffer (split-window nil
                                             (- (window-height)
                                                pinentry-prompt-window-height))
			       pinentry--prompt-buffer)
	  (pop-to-buffer pinentry--prompt-buffer)
	  (if (> (window-height) pinentry-prompt-window-height)
	      (shrink-window (- (window-height)
                                pinentry-prompt-window-height))))
        (prog1 (apply query-function prompt query-args)
          (quit-window)))
      (apply query-function (concat desc "\n" prompt) query-args))))

;;;###autoload
(defun pinentry-start (&optional quiet)
  "Start a Pinentry service.

Once the environment is properly set, subsequent invocations of
the gpg command will interact with Emacs for passphrase input.

If the optional QUIET argument is non-nil, messages at startup
will not be shown."
  (interactive)
  (unless (featurep 'make-network-process '(:family local))
    (error "local sockets are not supported"))
  (if (process-live-p pinentry--server-process)
      (unless quiet
        (message "Pinentry service is already running"))
    (let* ((server-file (expand-file-name "pinentry" pinentry--socket-dir)))
      (server-ensure-safe-dir pinentry--socket-dir)
      ;; Delete the socket files made by previous server invocations.
      (ignore-errors
        (let (delete-by-moving-to-trash)
          (delete-file server-file)))
      (cl-letf (((default-file-modes) ?\700))
        (setq pinentry--server-process
              (make-network-process
               :name "pinentry"
               :server t
               :noquery t
               :sentinel #'pinentry--process-sentinel
               :filter #'pinentry--process-filter
               :coding 'no-conversion
               :family 'local
               :service server-file))
        (process-put pinentry--server-process :server-file server-file)))))

(defun pinentry-stop ()
  "Stop a Pinentry service."
  (interactive)
  (when (process-live-p pinentry--server-process)
    (delete-process pinentry--server-process))
  (setq pinentry--server-process nil)
  (dolist (process pinentry--connection-process-list)
    (when (buffer-live-p (process-buffer process))
      (kill-buffer (process-buffer process))))
  (setq pinentry--connection-process-list nil))

(defun pinentry--labels-to-shortcuts (labels)
  "Convert strings in LABEL by stripping mnemonics."
  (mapcar (lambda (label)
            (when label
              (let (c)
                (if (string-match "\\(?:\\`\\|[^_]\\)_\\([[:alnum:]]\\)" label)
                    (let ((key (match-string 1 label)))
                      (setq c (downcase (aref key 0)))
                      (setq label (replace-match
                                   (propertize key 'face 'underline)
                                   t t label)))
                  (setq c (if (= (length label) 0)
                              ??
                            (downcase (aref label 0)))))
                ;; Double underscores mean a single underscore.
                (when (string-match "__" label)
                  (setq label (replace-match "_" t t label)))
                (cons c label))))
          labels))

(defun pinentry--escape-string (string)
  "Escape STRING in the Assuan percent escape."
  (let ((length (length string))
        (index 0)
        (count 0))
    (while (< index length)
      (if (memq (aref string index) '(?\n ?\r ?%))
          (setq count (1+ count)))
      (setq index (1+ index)))
    (setq index 0)
    (let ((result (make-string (+ length (* count 2)) ?\0))
          (result-index 0)
          c)
      (while (< index length)
        (setq c (aref string index))
        (if (memq c '(?\n ?\r ?%))
            (let ((hex (format "%02X" c)))
              (aset result result-index ?%)
              (setq result-index (1+ result-index))
              (aset result result-index (aref hex 0))
              (setq result-index (1+ result-index))
              (aset result result-index (aref hex 1))
              (setq result-index (1+ result-index)))
          (aset result result-index c)
          (setq result-index (1+ result-index)))
        (setq index (1+ index)))
      result)))

(defun pinentry--unescape-string (string)
  "Unescape STRING in the Assuan percent escape."
  (let ((length (length string))
        (index 0))
    (let ((result (make-string length ?\0))
          (result-index 0)
          c)
      (while (< index length)
        (setq c (aref string index))
        (if (and (eq c '?%) (< (+ index 2) length))
	    (progn
	      (aset result result-index
		    (string-to-number (substring string
						 (1+ index)
						 (+ index 3))
				      16))
	      (setq result-index (1+ result-index))
	      (setq index (+ index 2)))
          (aset result result-index c)
          (setq result-index (1+ result-index)))
	(setq index (1+ index)))
      (substring result 0 result-index))))

(defun pinentry--send-data (process escaped)
  "Send a string ESCAPED to a process PROCESS.
ESCAPED will be split if it exceeds the line length limit of the
Assuan protocol."
  (let ((length (length escaped))
        (index 0))
    (if (= length 0)
        (process-send-string process "D \n")
      (while (< index length)
        ;; 997 = ASSUAN_LINELENGTH (= 1000) - strlen ("D \n")
        (let* ((sub-length (min (- length index) 997))
               (sub (substring escaped index (+ index sub-length))))
          (unwind-protect
              (progn
                (process-send-string process "D ")
                (process-send-string process sub)
                (process-send-string process "\n"))
            (clear-string sub))
          (setq index (+ index sub-length)))))))

(defun pinentry--send-error (process error)
  (process-send-string process (format "ERR %d %s\n" (car error) (cdr error))))

(defun pinentry--process-filter (process input)
  (unless (buffer-live-p (process-buffer process))
    (let ((buffer (generate-new-buffer " *pinentry*")))
      (set-process-buffer process buffer)
      (with-current-buffer buffer
        (if (fboundp 'set-buffer-multibyte)
            (set-buffer-multibyte nil))
        (make-local-variable 'pinentry--read-point)
        (setq pinentry--read-point (point-min))
        (make-local-variable 'pinentry--labels))))
  (with-current-buffer (process-buffer process)
    (when pinentry-debug
      (with-current-buffer
          (or pinentry-debug-buffer
              (setq pinentry-debug-buffer (generate-new-buffer
                                           " *pinentry-debug*")))
        (goto-char (point-max))
        (insert input)))
    (save-excursion
      (goto-char (point-max))
      (insert input)
      (goto-char pinentry--read-point)
      (beginning-of-line)
      (while (looking-at ".*\n")        ;the input line finished
        (if (looking-at "\\([A-Z_]+\\) ?\\(.*\\)")
            (let ((command (match-string 1))
                  (string (pinentry--unescape-string (match-string 2))))
              (pcase command
                ((and set (guard (member set pinentry--set-label-commands)))
		 (when (> (length string) 0)
		   (let* ((symbol (intern (downcase (substring set 3))))
			  (entry (assq symbol pinentry--labels))
			  (label (decode-coding-string string 'utf-8)))
		     (if entry
			 (setcdr entry label)
		       (push (cons symbol label) pinentry--labels))))
		 (ignore-errors
		   (process-send-string process "OK\n")))
		("NOP"
		 (ignore-errors
		   (process-send-string process "OK\n")))
                ("GETPIN"
                 (let ((confirm (not (null (assq 'repeat pinentry--labels))))
                       passphrase escaped-passphrase encoded-passphrase)
                   (unwind-protect
                       (condition-case err
                           (progn
                             (setq passphrase
                                   (pinentry--prompt
                                    pinentry--labels
                                    #'read-passwd confirm))
                               (setq escaped-passphrase
                                     (pinentry--escape-string
                                      passphrase))
                               (setq encoded-passphrase (encode-coding-string
                                                         escaped-passphrase
                                                         'utf-8))
			       (ignore-errors
				 (pinentry--send-data
				  process encoded-passphrase)
				 (process-send-string process "OK\n")))
                         (error
                          (message "GETPIN error %S" err)
			    (ignore-errors
			      (pinentry--send-error
			       process
			       pinentry--error-cancelled))))
                       (if passphrase
                           (clear-string passphrase))
                       (if escaped-passphrase
                           (clear-string escaped-passphrase))
                       (if encoded-passphrase
                           (clear-string encoded-passphrase))))
                   (setq pinentry--labels nil))
                ("CONFIRM"
                 (let ((prompt
                        (or (cdr (assq 'prompt pinentry--labels))
                            "Confirm? "))
                       (buttons
                        (delq nil
                              (pinentry--labels-to-shortcuts
                               (list (cdr (assq 'ok pinentry--labels))
                                     (cdr (assq 'notok pinentry--labels))
                                     (cdr (assq 'cancel pinentry--labels))))))
                       entry)
                   (if buttons
                       (progn
                         (setq prompt
                               (concat prompt " ("
                                       (mapconcat #'cdr buttons
                                                  ", ")
                                       ") "))
                         (if (setq entry (assq 'prompt pinentry--labels))
                             (setcdr entry prompt)
                           (setq pinentry--labels (cons (cons 'prompt prompt)
                                                        pinentry--labels)))
                         (condition-case nil
                             (let ((result (pinentry--prompt pinentry--labels
                                                             #'read-char)))
                               (if (eq result (caar buttons))
                                   (ignore-errors
                                     (process-send-string process "OK\n"))
                                 (if (eq result (car (nth 1 buttons)))
                                     (ignore-errors
                                       (pinentry--send-error
                                        process
                                        pinentry--error-not-confirmed))
                                   (ignore-errors
                                     (pinentry--send-error
                                      process
                                      pinentry--error-cancelled)))))
                           (error
                            (ignore-errors
			      (pinentry--send-error
			       process
			       pinentry--error-cancelled)))))
                     (if (setq entry (assq 'prompt pinentry--labels))
                         (setcdr entry prompt)
                       (setq pinentry--labels (cons (cons 'prompt prompt)
                                                    pinentry--labels)))
                     (if (condition-case nil
                             (pinentry--prompt pinentry--labels #'y-or-n-p)
                           (quit))
			 (ignore-errors
			   (process-send-string process "OK\n"))
		       (ignore-errors
			 (pinentry--send-error
			  process
			  pinentry--error-not-confirmed))))
                   (setq pinentry--labels nil)))
                (_ (ignore-errors
		     (pinentry--send-error
		      process
		      pinentry--error-not-implemented))))
              (forward-line)
              (setq pinentry--read-point (point))))))))

(defun pinentry--process-sentinel (process _status)
  "The process sentinel for Emacs server connections."
  ;; If this is a new client process, set the query-on-exit flag to nil
  ;; for this process (it isn't inherited from the server process).
  (when (and (eq (process-status process) 'open)
	     (process-query-on-exit-flag process))
    (push process pinentry--connection-process-list)
    (set-process-query-on-exit-flag process nil)
    (ignore-errors
      (process-send-string process "OK Your orders please\n")))
  ;; Kill the process buffer of the connection process.
  (when (and (not (process-contact process :server))
	     (eq (process-status process) 'closed))
    (when (buffer-live-p (process-buffer process))
      (kill-buffer (process-buffer process)))
    (setq pinentry--connection-process-list
	  (delq process pinentry--connection-process-list)))
  ;; Delete the associated connection file, if applicable.
  ;; Although there's no 100% guarantee that the file is owned by the
  ;; running Emacs instance, server-start uses server-running-p to check
  ;; for possible servers before doing anything, so it *should* be ours.
  (and (process-contact process :server)
       (eq (process-status process) 'closed)
       (ignore-errors
	 (delete-file (process-get process :server-file)))))

(provide 'pinentry)

;;; pinentry.el ends here