360388d002e4ad4ef0cfc4174e75bc06fe833872
[gnus] / lisp / plstore.el
1 ;;; plstore.el --- searchable, partially encrypted, persistent plist store -*- lexical-binding: t -*-
2 ;; Copyright (C) 2011 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 ;; Creating:
25 ;;
26 ;; (setq store (plstore-open (expand-file-name "~/.emacs.d/auth.plist")))
27 ;; (plstore-put store "foo" '(:host "foo.example.org" :port 80) nil)
28 ;; (plstore-save store)
29 ;; ;; :user property is secret
30 ;; (plstore-put store "bar" '(:host "bar.example.org") '(:user "test"))
31 ;; (plstore-put store "baz" '(:host "baz.example.org") '(:user "test"))
32 ;; (plstore-save store) ;<= will ask passphrase via GPG
33 ;; (plstore-close store)
34 ;;
35 ;; Searching:
36 ;;
37 ;; (setq store (plstore-open (expand-file-name "~/.emacs.d/auth.plist")))
38 ;; (plstore-find store '(:host ("foo.example.org")))
39 ;; (plstore-find store '(:host ("bar.example.org"))) ;<= will ask passphrase via GPG
40 ;; (plstore-close store)
41 ;;
42
43 ;;; Code:
44
45 (require 'epg)
46
47 (defgroup plstore nil
48   "Searchable, partially encrypted, persistent plist store"
49   :version "24.1"
50   :group 'files)
51
52 (defcustom plstore-select-keys 'silent
53   "Control whether or not to pop up the key selection dialog.
54
55 If t, always asks user to select recipients.
56 If nil, query user only when `plstore-encrypt-to' is not set.
57 If neither t nor nil, doesn't ask user.  In this case, symmetric
58 encryption is used."
59   :type '(choice (const :tag "Ask always" t)
60                  (const :tag "Ask when recipients are not set" nil)
61                  (const :tag "Don't ask" silent))
62   :group 'plstore)
63
64 (defvar plstore-encrypt-to nil
65   "*Recipient(s) used for encrypting secret entries.
66 May either be a string or a list of strings.")
67
68 (put 'plstore-encrypt-to 'safe-local-variable
69      (lambda (val)
70        (or (stringp val)
71            (and (listp val)
72                 (catch 'safe
73                   (mapc (lambda (elt)
74                           (unless (stringp elt)
75                             (throw 'safe nil)))
76                         val)
77                   t)))))
78
79 (put 'plstore-encrypt-to 'permanent-local t)
80
81 (defvar plstore-cache-passphrase-for-symmetric-encryption nil)
82 (defvar plstore-passphrase-alist nil)
83
84 (defun plstore-passphrase-callback-function (_context _key-id plstore)
85   (if plstore-cache-passphrase-for-symmetric-encryption
86       (let* ((file (file-truename (plstore--get-buffer plstore)))
87              (entry (assoc file plstore-passphrase-alist))
88              passphrase)
89         (or (copy-sequence (cdr entry))
90             (progn
91               (unless entry
92                 (setq entry (list file)
93                       plstore-passphrase-alist
94                       (cons entry
95                             plstore-passphrase-alist)))
96               (setq passphrase
97                     (read-passwd (format "Passphrase for PLSTORE %s: "
98                                          (plstore--get-buffer plstore))))
99               (setcdr entry (copy-sequence passphrase))
100               passphrase)))
101     (read-passwd (format "Passphrase for PLSTORE %s: "
102                          (plstore--get-buffer plstore)))))
103
104 (defun plstore-progress-callback-function (_context _what _char current total
105                                                     handback)
106   (if (= current total)
107       (message "%s...done" handback)
108     (message "%s...%d%%" handback
109              (if (> total 0) (floor (* (/ current (float total)) 100)) 0))))
110
111 (defun plstore--get-buffer (this)
112   (aref this 0))
113
114 (defun plstore--get-alist (this)
115   (aref this 1))
116
117 (defun plstore--get-encrypted-data (this)
118   (aref this 2))
119
120 (defun plstore--get-secret-alist (this)
121   (aref this 3))
122
123 (defun plstore--get-merged-alist (this)
124   (aref this 4))
125
126 (defun plstore--set-file (this file)
127   (aset this 0 file))
128
129 (defun plstore--set-alist (this plist)
130   (aset this 1 plist))
131
132 (defun plstore--set-encrypted-data (this encrypted-data)
133   (aset this 2 encrypted-data))
134
135 (defun plstore--set-secret-alist (this secret-alist)
136   (aset this 3 secret-alist))
137
138 (defun plstore--set-merged-alist (this merged-alist)
139   (aset this 4 merged-alist))
140
141 (defun plstore-get-file (this)
142   (buffer-file-name (plstore--get-buffer this)))
143
144 (defun plstore--init-from-buffer (plstore)
145   (goto-char (point-min))
146   (when (looking-at ";;; public entries")
147     (forward-line)
148     (plstore--set-alist plstore (read (point-marker)))
149     (forward-sexp)
150     (forward-char)
151     (when (looking-at ";;; secret entries")
152       (forward-line)
153       (plstore--set-encrypted-data plstore (read (point-marker))))
154     (plstore--merge-secret plstore)))
155
156 ;;;###autoload
157 (defun plstore-open (file)
158   "Create a plstore instance associated with FILE."
159   (with-current-buffer (find-file-noselect file)
160     ;; make the buffer invisible from user
161     (rename-buffer (format " plstore %s" (buffer-file-name)))
162     (let ((store (vector
163                   (current-buffer)
164                   nil                ;plist (plist)
165                   nil                ;encrypted data (string)
166                   nil                ;secret plist (plist)
167                   nil                ;merged plist (plist)
168                   )))
169       (plstore--init-from-buffer store)
170       store)))
171
172 (defun plstore-revert (plstore)
173   "Replace current data in PLSTORE with the file on disk."
174   (with-current-buffer (plstore--get-buffer plstore)
175     (revert-buffer t t)
176     (plstore--init-from-buffer plstore)))
177
178 (defun plstore-close (plstore)
179   "Destroy a plstore instance PLSTORE."
180   (kill-buffer (plstore--get-buffer plstore)))
181
182 (defun plstore--merge-secret (plstore)
183   (let ((alist (plstore--get-secret-alist plstore))
184         modified-alist
185         modified-plist
186         modified-entry
187         entry
188         plist
189         placeholder)
190     (plstore--set-merged-alist
191      plstore
192      (copy-tree (plstore--get-alist plstore)))
193     (setq modified-alist (plstore--get-merged-alist plstore))
194     (while alist
195       (setq entry (car alist)
196             alist (cdr alist)
197             plist (cdr entry)
198             modified-entry (assoc (car entry) modified-alist)
199             modified-plist (cdr modified-entry))
200       (while plist
201         (setq placeholder
202               (plist-member
203                modified-plist
204                (intern (concat ":secret-"
205                                (substring (symbol-name (car plist)) 1)))))
206         (if placeholder
207             (setcar placeholder (car plist)))
208         (setq modified-plist
209               (plist-put modified-plist (car plist) (car (cdr plist))))
210         (setq plist (nthcdr 2 plist)))
211       (setcdr modified-entry modified-plist))))
212
213 (defun plstore--decrypt (plstore)
214   (if (plstore--get-encrypted-data plstore)
215       (let ((context (epg-make-context 'OpenPGP))
216             plain)
217         (epg-context-set-passphrase-callback
218          context
219          (cons #'plstore-passphrase-callback-function
220                plstore))
221         (epg-context-set-progress-callback
222          context
223          (cons #'plstore-progress-callback-function
224                (format "Decrypting %s" (plstore-get-file plstore))))
225         (setq plain
226               (epg-decrypt-string context
227                                   (plstore--get-encrypted-data plstore)))
228         (plstore--set-secret-alist plstore (car (read-from-string plain)))
229         (plstore--merge-secret plstore)
230         (plstore--set-encrypted-data plstore nil))))
231
232 (defun plstore--match (entry keys skip-if-secret-found)
233   (let ((result t) key-name key-value prop-value secret-name)
234     (while keys
235       (setq key-name (car keys)
236             key-value (car (cdr keys))
237             prop-value (plist-get (cdr entry) key-name))
238         (unless (member prop-value key-value)
239           (if skip-if-secret-found
240               (progn
241                 (setq secret-name
242                       (intern (concat ":secret-"
243                                       (substring (symbol-name key-name) 1))))
244                 (if (plist-member (cdr entry) secret-name)
245                     (setq result 'secret)
246                   (setq result nil
247                         keys nil)))
248             (setq result nil
249                   keys nil)))
250         (setq keys (nthcdr 2 keys)))
251     result))
252
253 (defun plstore-find (plstore keys)
254   "Perform search on PLSTORE with KEYS.
255 KEYS is a plist."
256   (let (entries alist entry match decrypt plist)
257     ;; First, go through the merged plist alist and collect entries
258     ;; matched with keys.
259     (setq alist (plstore--get-merged-alist plstore))
260     (while alist
261       (setq entry (car alist)
262             alist (cdr alist)
263             match (plstore--match entry keys t))
264       (if (eq match 'secret)
265           (setq decrypt t)
266         (when match
267           (setq plist (cdr entry))
268           (while plist
269             (if (string-match "\\`:secret-" (symbol-name (car plist)))
270                 (setq decrypt t
271                       plist nil))
272             (setq plist (nthcdr 2 plist)))
273           (setq entries (cons entry entries)))))
274     ;; Second, decrypt the encrypted plist and try again.
275     (when decrypt
276       (setq entries nil)
277       (plstore--decrypt plstore)
278       (setq alist (plstore--get-merged-alist plstore))
279       (while alist
280         (setq entry (car alist)
281               alist (cdr alist)
282               match (plstore--match entry keys nil))
283         (if match
284             (setq entries (cons entry entries)))))
285     (nreverse entries)))
286
287 (defun plstore-get (plstore name)
288   "Get an entry with NAME in PLSTORE."
289   (let ((entry (assoc name (plstore--get-merged-alist plstore)))
290         plist)
291     (setq plist (cdr entry))
292     (while plist
293       (if (string-match "\\`:secret-" (symbol-name (car plist)))
294           (progn
295             (plstore--decrypt plstore)
296             (setq entry (assoc name (plstore--get-merged-alist plstore))
297                   plist nil))
298         (setq plist (nthcdr 2 plist))))
299     entry))
300
301 (defun plstore-put (plstore name keys secret-keys)
302   "Put an entry with NAME in PLSTORE.
303 KEYS is a plist containing non-secret data.
304 SECRET-KEYS is a plist containing secret data."
305   (let (entry
306         plist
307         secret-plist
308         symbol)
309     (if secret-keys
310         (plstore--decrypt plstore))
311     (while secret-keys
312       (setq symbol
313             (intern (concat ":secret-"
314                             (substring (symbol-name (car secret-keys)) 1))))
315       (setq plist (plist-put plist symbol t)
316             secret-plist (plist-put secret-plist
317                                     (car secret-keys) (car (cdr secret-keys)))
318             secret-keys (nthcdr 2 secret-keys)))
319     (while keys
320       (setq symbol
321             (intern (concat ":secret-"
322                             (substring (symbol-name (car keys)) 1))))
323       (setq plist (plist-put plist (car keys) (car (cdr keys)))
324             keys (nthcdr 2 keys)))
325     (setq entry (assoc name (plstore--get-alist plstore)))
326     (if entry
327         (setcdr entry plist)
328       (plstore--set-alist
329        plstore
330        (cons (cons name plist) (plstore--get-alist plstore))))
331     (when secret-plist
332       (setq entry (assoc name (plstore--get-secret-alist plstore)))
333       (if entry
334           (setcdr entry secret-plist)
335         (plstore--set-secret-alist
336          plstore
337          (cons (cons name secret-plist) (plstore--get-secret-alist plstore)))))
338     (plstore--merge-secret plstore)))
339
340 (defvar pp-escape-newlines)
341 (defun plstore-save (plstore)
342   "Save the contents of PLSTORE associated with a FILE."
343   (with-current-buffer (plstore--get-buffer plstore)
344     (erase-buffer)
345     (insert ";;; public entries -*- mode: emacs-lisp -*- \n"
346             (pp-to-string (plstore--get-alist plstore)))
347     (if (plstore--get-secret-alist plstore)
348         (let ((context (epg-make-context 'OpenPGP))
349               (pp-escape-newlines nil)
350               (recipients
351                (cond
352                 ((listp plstore-encrypt-to) plstore-encrypt-to)
353                 ((stringp plstore-encrypt-to) (list plstore-encrypt-to))))
354               cipher)
355           (epg-context-set-armor context t)
356           (epg-context-set-passphrase-callback
357            context
358            (cons #'plstore-passphrase-callback-function
359                  plstore))
360           (setq cipher (epg-encrypt-string
361                         context
362                         (pp-to-string
363                          (plstore--get-secret-alist plstore))
364                         (if (or (eq plstore-select-keys t)
365                                 (and (null plstore-select-keys)
366                                      (not (local-variable-p 'plstore-encrypt-to
367                                                             (current-buffer)))))
368                             (epa-select-keys
369                              context
370                              "Select recipents for encryption.
371 If no one is selected, symmetric encryption will be performed.  "
372                              recipients)
373                           (if plstore-encrypt-to
374                               (epg-list-keys context recipients)))))
375           (goto-char (point-max))
376           (insert ";;; secret entries\n" (pp-to-string cipher))))
377     (save-buffer)))
378
379 (provide 'plstore)
380
381 ;;; plstore.el ends here