Merge remote-tracking branch 'origin/no-gnus'
[gnus] / lisp / plstore.el
1 ;;; plstore.el --- secure plist store -*- lexical-binding: t -*-
2 ;; Copyright (C) 2011-2012 Free Software Foundation, Inc.
3
4 ;; Author: Daiki Ueno <ueno@unixuser.org>
5 ;; Keywords: PGP, GnuPG
6
7 ;; This file is part of GNU Emacs.
8
9 ;; GNU Emacs is free software: you can redistribute it and/or modify
10 ;; it under the terms of the GNU General Public License as published by
11 ;; the Free Software Foundation, either version 3 of the License, or
12 ;; (at your option) any later version.
13
14 ;; GNU Emacs is distributed in the hope that it will be useful,
15 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 ;; GNU General Public License for more details.
18
19 ;; You should have received a copy of the GNU General Public License
20 ;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
21
22 ;;; Commentary
23
24 ;; Plist based data store providing search and partial encryption.
25 ;;
26 ;; Creating:
27 ;;
28 ;; ;; Open a new store associated with ~/.emacs.d/auth.plist.
29 ;; (setq store (plstore-open (expand-file-name "~/.emacs.d/auth.plist")))
30 ;; ;; Both `:host' and `:port' are public property.
31 ;; (plstore-put store "foo" '(:host "foo.example.org" :port 80) nil)
32 ;; ;; No encryption will be needed.
33 ;; (plstore-save store)
34 ;;
35 ;; ;; `:user' is marked as secret.
36 ;; (plstore-put store "bar" '(:host "bar.example.org") '(:user "test"))
37 ;; ;; `:password' is marked as secret.
38 ;; (plstore-put store "baz" '(:host "baz.example.org") '(:password "test"))
39 ;; ;; Those secret properties are encrypted together.
40 ;; (plstore-save store)
41 ;;
42 ;; ;; Kill the buffer visiting ~/.emacs.d/auth.plist.
43 ;; (plstore-close store)
44 ;;
45 ;; Searching:
46 ;;
47 ;; (setq store (plstore-open (expand-file-name "~/.emacs.d/auth.plist")))
48 ;;
49 ;; ;; As the entry "foo" associated with "foo.example.org" has no
50 ;; ;; secret properties, no need to decryption.
51 ;; (plstore-find store '(:host ("foo.example.org")))
52 ;;
53 ;; ;; As the entry "bar" associated with "bar.example.org" has a
54 ;; ;; secret property `:user', Emacs tries to decrypt the secret (and
55 ;; ;; thus you will need to input passphrase).
56 ;; (plstore-find store '(:host ("bar.example.org")))
57 ;;
58 ;; ;; While the entry "baz" associated with "baz.example.org" has also
59 ;; ;; a secret property `:password', it is encrypted together with
60 ;; ;; `:user' of "bar", so no need to decrypt the secret.
61 ;; (plstore-find store '(:host ("bar.example.org")))
62 ;;
63 ;; (plstore-close store)
64 ;;
65 ;; Editing:
66 ;;
67 ;; Currently not supported but in the future plstore will provide a
68 ;; major mode to edit PLSTORE files.
69
70 ;;; Code:
71
72 (require 'epg)
73
74 (defgroup plstore nil
75   "Searchable, partially encrypted, persistent plist store"
76   :version "24.1"
77   :group 'files)
78
79 (defcustom plstore-select-keys 'silent
80   "Control whether or not to pop up the key selection dialog.
81
82 If t, always asks user to select recipients.
83 If nil, query user only when a file's default recipients are not
84 known (i.e. `plstore-encrypt-to' is not locally set in the buffer
85 visiting a plstore file).
86 If neither t nor nil, doesn't ask user."
87   :type '(choice (const :tag "Ask always" t)
88                  (const :tag "Ask when recipients are not set" nil)
89                  (const :tag "Don't ask" silent))
90   :group 'plstore)
91
92 (defvar plstore-encrypt-to nil
93   "*Recipient(s) used for encrypting secret entries.
94 May either be a string or a list of strings.  If it is nil,
95 symmetric encryption will be used.")
96
97 (put 'plstore-encrypt-to 'safe-local-variable
98      (lambda (val)
99        (or (stringp val)
100            (and (listp val)
101                 (catch 'safe
102                   (mapc (lambda (elt)
103                           (unless (stringp elt)
104                             (throw 'safe nil)))
105                         val)
106                   t)))))
107
108 (put 'plstore-encrypt-to 'permanent-local t)
109
110 (defvar plstore-cache-passphrase-for-symmetric-encryption nil)
111 (defvar plstore-passphrase-alist nil)
112
113 (defun plstore-passphrase-callback-function (_context _key-id plstore)
114   (if plstore-cache-passphrase-for-symmetric-encryption
115       (let* ((file (file-truename (plstore--get-buffer plstore)))
116              (entry (assoc file plstore-passphrase-alist))
117              passphrase)
118         (or (copy-sequence (cdr entry))
119             (progn
120               (unless entry
121                 (setq entry (list file)
122                       plstore-passphrase-alist
123                       (cons entry
124                             plstore-passphrase-alist)))
125               (setq passphrase
126                     (read-passwd (format "Passphrase for PLSTORE %s: "
127                                          (plstore--get-buffer plstore))))
128               (setcdr entry (copy-sequence passphrase))
129               passphrase)))
130     (read-passwd (format "Passphrase for PLSTORE %s: "
131                          (plstore--get-buffer plstore)))))
132
133 (defun plstore-progress-callback-function (_context _what _char current total
134                                                     handback)
135   (if (= current total)
136       (message "%s...done" handback)
137     (message "%s...%d%%" handback
138              (if (> total 0) (floor (* (/ current (float total)) 100)) 0))))
139
140 (defun plstore--get-buffer (arg)
141   (aref arg 0))
142
143 (defun plstore--get-alist (arg)
144   (aref arg 1))
145
146 (defun plstore--get-encrypted-data (arg)
147   (aref arg 2))
148
149 (defun plstore--get-secret-alist (arg)
150   (aref arg 3))
151
152 (defun plstore--get-merged-alist (arg)
153   (aref arg 4))
154
155 (defun plstore--set-buffer (arg buffer)
156   (aset arg 0 buffer))
157
158 (defun plstore--set-alist (arg plist)
159   (aset arg 1 plist))
160
161 (defun plstore--set-encrypted-data (arg encrypted-data)
162   (aset arg 2 encrypted-data))
163
164 (defun plstore--set-secret-alist (arg secret-alist)
165   (aset arg 3 secret-alist))
166
167 (defun plstore--set-merged-alist (arg merged-alist)
168   (aset arg 4 merged-alist))
169
170 (defun plstore-get-file (arg)
171   (buffer-file-name (plstore--get-buffer arg)))
172
173 (defun plstore--make (&optional buffer alist encrypted-data secret-alist
174                                 merged-alist)
175   (vector buffer alist encrypted-data secret-alist merged-alist))
176
177 (defun plstore--init-from-buffer (plstore)
178   (goto-char (point-min))
179   (when (looking-at ";;; public entries")
180     (forward-line)
181     (plstore--set-alist plstore (read (point-marker)))
182     (forward-sexp)
183     (forward-char)
184     (when (looking-at ";;; secret entries")
185       (forward-line)
186       (plstore--set-encrypted-data plstore (read (point-marker))))
187     (plstore--merge-secret plstore)))
188
189 ;;;###autoload
190 (defun plstore-open (file)
191   "Create a plstore instance associated with FILE."
192   (let* ((filename (file-truename file))
193          (buffer (or (find-buffer-visiting filename)
194                      (generate-new-buffer (format " plstore %s" filename))))
195          (store (plstore--make buffer)))
196     (with-current-buffer buffer
197       ;; In the future plstore will provide a major mode called
198       ;; `plstore-mode' to edit PLSTORE files.
199       (if (eq major-mode 'plstore-mode)
200           (error "%s is opened for editing; kill the buffer first" file))
201       (erase-buffer)
202       (condition-case nil
203           (insert-file-contents-literally file)
204         (error))
205       (setq buffer-file-name (file-truename file))
206       (set-buffer-modified-p nil)
207       (plstore--init-from-buffer store)
208       store)))
209
210 (defun plstore-revert (plstore)
211   "Replace current data in PLSTORE with the file on disk."
212   (with-current-buffer (plstore--get-buffer plstore)
213     (revert-buffer t t)
214     (plstore--init-from-buffer plstore)))
215
216 (defun plstore-close (plstore)
217   "Destroy a plstore instance PLSTORE."
218   (kill-buffer (plstore--get-buffer plstore)))
219
220 (defun plstore--merge-secret (plstore)
221   (let ((alist (plstore--get-secret-alist plstore))
222         modified-alist
223         modified-plist
224         modified-entry
225         entry
226         plist
227         placeholder)
228     (plstore--set-merged-alist
229      plstore
230      (copy-tree (plstore--get-alist plstore)))
231     (setq modified-alist (plstore--get-merged-alist plstore))
232     (while alist
233       (setq entry (car alist)
234             alist (cdr alist)
235             plist (cdr entry)
236             modified-entry (assoc (car entry) modified-alist)
237             modified-plist (cdr modified-entry))
238       (while plist
239         (setq placeholder
240               (plist-member
241                modified-plist
242                (intern (concat ":secret-"
243                                (substring (symbol-name (car plist)) 1)))))
244         (if placeholder
245             (setcar placeholder (car plist)))
246         (setq modified-plist
247               (plist-put modified-plist (car plist) (car (cdr plist))))
248         (setq plist (nthcdr 2 plist)))
249       (setcdr modified-entry modified-plist))))
250
251 (defun plstore--decrypt (plstore)
252   (if (plstore--get-encrypted-data plstore)
253       (let ((context (epg-make-context 'OpenPGP))
254             plain)
255         (epg-context-set-passphrase-callback
256          context
257          (cons #'plstore-passphrase-callback-function
258                plstore))
259         (epg-context-set-progress-callback
260          context
261          (cons #'plstore-progress-callback-function
262                (format "Decrypting %s" (plstore-get-file plstore))))
263         (setq plain
264               (epg-decrypt-string context
265                                   (plstore--get-encrypted-data plstore)))
266         (plstore--set-secret-alist plstore (car (read-from-string plain)))
267         (plstore--merge-secret plstore)
268         (plstore--set-encrypted-data plstore nil))))
269
270 (defun plstore--match (entry keys skip-if-secret-found)
271   (let ((result t) key-name key-value prop-value secret-name)
272     (while keys
273       (setq key-name (car keys)
274             key-value (car (cdr keys))
275             prop-value (plist-get (cdr entry) key-name))
276         (unless (member prop-value key-value)
277           (if skip-if-secret-found
278               (progn
279                 (setq secret-name
280                       (intern (concat ":secret-"
281                                       (substring (symbol-name key-name) 1))))
282                 (if (plist-member (cdr entry) secret-name)
283                     (setq result 'secret)
284                   (setq result nil
285                         keys nil)))
286             (setq result nil
287                   keys nil)))
288         (setq keys (nthcdr 2 keys)))
289     result))
290
291 (defun plstore-find (plstore keys)
292   "Perform search on PLSTORE with KEYS.
293 KEYS is a plist."
294   (let (entries alist entry match decrypt plist)
295     ;; First, go through the merged plist alist and collect entries
296     ;; matched with keys.
297     (setq alist (plstore--get-merged-alist plstore))
298     (while alist
299       (setq entry (car alist)
300             alist (cdr alist)
301             match (plstore--match entry keys t))
302       (if (eq match 'secret)
303           (setq decrypt t)
304         (when match
305           (setq plist (cdr entry))
306           (while plist
307             (if (string-match "\\`:secret-" (symbol-name (car plist)))
308                 (setq decrypt t
309                       plist nil))
310             (setq plist (nthcdr 2 plist)))
311           (setq entries (cons entry entries)))))
312     ;; Second, decrypt the encrypted plist and try again.
313     (when decrypt
314       (setq entries nil)
315       (plstore--decrypt plstore)
316       (setq alist (plstore--get-merged-alist plstore))
317       (while alist
318         (setq entry (car alist)
319               alist (cdr alist)
320               match (plstore--match entry keys nil))
321         (if match
322             (setq entries (cons entry entries)))))
323     (nreverse entries)))
324
325 (defun plstore-get (plstore name)
326   "Get an entry with NAME in PLSTORE."
327   (let ((entry (assoc name (plstore--get-merged-alist plstore)))
328         plist)
329     (setq plist (cdr entry))
330     (while plist
331       (if (string-match "\\`:secret-" (symbol-name (car plist)))
332           (progn
333             (plstore--decrypt plstore)
334             (setq entry (assoc name (plstore--get-merged-alist plstore))
335                   plist nil))
336         (setq plist (nthcdr 2 plist))))
337     entry))
338
339 (defun plstore-put (plstore name keys secret-keys)
340   "Put an entry with NAME in PLSTORE.
341 KEYS is a plist containing non-secret data.
342 SECRET-KEYS is a plist containing secret data."
343   (let (entry
344         plist
345         secret-plist
346         symbol)
347     (if secret-keys
348         (plstore--decrypt plstore))
349     (while secret-keys
350       (setq symbol
351             (intern (concat ":secret-"
352                             (substring (symbol-name (car secret-keys)) 1))))
353       (setq plist (plist-put plist symbol t)
354             secret-plist (plist-put secret-plist
355                                     (car secret-keys) (car (cdr secret-keys)))
356             secret-keys (nthcdr 2 secret-keys)))
357     (while keys
358       (setq symbol
359             (intern (concat ":secret-"
360                             (substring (symbol-name (car keys)) 1))))
361       (setq plist (plist-put plist (car keys) (car (cdr keys)))
362             keys (nthcdr 2 keys)))
363     (setq entry (assoc name (plstore--get-alist plstore)))
364     (if entry
365         (setcdr entry plist)
366       (plstore--set-alist
367        plstore
368        (cons (cons name plist) (plstore--get-alist plstore))))
369     (when secret-plist
370       (setq entry (assoc name (plstore--get-secret-alist plstore)))
371       (if entry
372           (setcdr entry secret-plist)
373         (plstore--set-secret-alist
374          plstore
375          (cons (cons name secret-plist) (plstore--get-secret-alist plstore)))))
376     (plstore--merge-secret plstore)))
377
378 (defun plstore-delete (plstore name)
379   "Delete an entry with NAME from PLSTORE."
380   (let ((entry (assoc name (plstore--get-alist plstore))))
381     (if entry
382         (plstore--set-alist
383          plstore
384          (delq entry (plstore--get-alist plstore))))
385     (setq entry (assoc name (plstore--get-secret-alist plstore)))
386     (if entry
387         (plstore--set-secret-alist
388          plstore
389          (delq entry (plstore--get-secret-alist plstore))))
390     (setq entry (assoc name (plstore--get-merged-alist plstore)))
391     (if entry
392         (plstore--set-merged-alist
393          plstore
394          (delq entry (plstore--get-merged-alist plstore))))))
395
396 (defvar pp-escape-newlines)
397 (defun plstore--insert-buffer (plstore)
398   (insert ";;; public entries -*- mode: plstore -*- \n"
399           (pp-to-string (plstore--get-alist plstore)))
400   (if (plstore--get-secret-alist plstore)
401       (let ((context (epg-make-context 'OpenPGP))
402             (pp-escape-newlines nil)
403             (recipients
404              (cond
405               ((listp plstore-encrypt-to) plstore-encrypt-to)
406               ((stringp plstore-encrypt-to) (list plstore-encrypt-to))))
407             cipher)
408         (epg-context-set-armor context t)
409         (epg-context-set-passphrase-callback
410          context
411          (cons #'plstore-passphrase-callback-function
412                plstore))
413         (setq cipher (epg-encrypt-string
414                       context
415                       (pp-to-string
416                        (plstore--get-secret-alist plstore))
417                       (if (or (eq plstore-select-keys t)
418                               (and (null plstore-select-keys)
419                                    (not (local-variable-p 'plstore-encrypt-to
420                                                           (current-buffer)))))
421                           (epa-select-keys
422                            context
423                            "Select recipients for encryption.
424 If no one is selected, symmetric encryption will be performed.  "
425                            recipients)
426                         (if plstore-encrypt-to
427                             (epg-list-keys context recipients)))))
428         (goto-char (point-max))
429         (insert ";;; secret entries\n" (pp-to-string cipher)))))
430
431 (defun plstore-save (plstore)
432   "Save the contents of PLSTORE associated with a FILE."
433   (with-current-buffer (plstore--get-buffer plstore)
434     (erase-buffer)
435     (plstore--insert-buffer plstore)
436     (save-buffer)))
437
438 (provide 'plstore)
439
440 ;;; plstore.el ends here