Commit | Line | Data |
---|---|---|
8977de27 DU |
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 | (defvar plstore-cache-passphrase-for-symmetric-encryption nil) | |
48 | (defvar plstore-passphrase-alist nil) | |
49 | ||
50 | (defun plstore-passphrase-callback-function (_context _key-id plstore) | |
51 | (if plstore-cache-passphrase-for-symmetric-encryption | |
52 | (let* ((file (file-truename (plstore--get-buffer plstore))) | |
53 | (entry (assoc file plstore-passphrase-alist)) | |
54 | passphrase) | |
55 | (or (copy-sequence (cdr entry)) | |
56 | (progn | |
57 | (unless entry | |
58 | (setq entry (list file) | |
59 | plstore-passphrase-alist | |
60 | (cons entry | |
61 | plstore-passphrase-alist))) | |
62 | (setq passphrase | |
63 | (read-passwd (format "Passphrase for PLSTORE %s: " | |
64 | (plstore--get-buffer plstore)))) | |
65 | (setcdr entry (copy-sequence passphrase)) | |
66 | passphrase))) | |
67 | (read-passwd (format "Passphrase for PLSTORE %s: " | |
68 | (plstore--get-buffer plstore))))) | |
69 | ||
70 | (defun plstore-progress-callback-function (_context _what _char current total | |
71 | handback) | |
72 | (if (= current total) | |
73 | (message "%s...done" handback) | |
74 | (message "%s...%d%%" handback | |
75 | (if (> total 0) (floor (* (/ current (float total)) 100)) 0)))) | |
76 | ||
77 | (defun plstore--get-buffer (this) | |
78 | (aref this 0)) | |
79 | ||
80 | (defun plstore--get-alist (this) | |
81 | (aref this 1)) | |
82 | ||
83 | (defun plstore--get-encrypted-data (this) | |
84 | (aref this 2)) | |
85 | ||
86 | (defun plstore--get-secret-alist (this) | |
87 | (aref this 3)) | |
88 | ||
89 | (defun plstore--get-merged-alist (this) | |
90 | (aref this 4)) | |
91 | ||
92 | (defun plstore--set-file (this file) | |
93 | (aset this 0 file)) | |
94 | ||
95 | (defun plstore--set-alist (this plist) | |
96 | (aset this 1 plist)) | |
97 | ||
98 | (defun plstore--set-encrypted-data (this encrypted-data) | |
99 | (aset this 2 encrypted-data)) | |
100 | ||
101 | (defun plstore--set-secret-alist (this secret-alist) | |
102 | (aset this 3 secret-alist)) | |
103 | ||
104 | (defun plstore--set-merged-alist (this merged-alist) | |
105 | (aset this 4 merged-alist)) | |
106 | ||
107 | (defun plstore-get-file (this) | |
108 | (buffer-file-name (plstore--get-buffer this))) | |
109 | ||
110 | ;;;###autoload | |
111 | (defun plstore-open (file) | |
112 | "Create a plstore instance associated with FILE." | |
113 | (let ((store (vector | |
114 | (find-file-noselect file) | |
115 | nil ;plist (plist) | |
116 | nil ;encrypted data (string) | |
117 | nil ;secret plist (plist) | |
118 | nil ;merged plist (plist) | |
119 | ))) | |
8a8cdb19 | 120 | (plstore-revert store) |
8977de27 DU |
121 | store)) |
122 | ||
8a8cdb19 DU |
123 | (defun plstore-revert (plstore) |
124 | "Replace current data in PLSTORE with the file on disk." | |
125 | (with-current-buffer (plstore--get-buffer plstore) | |
f11f303b | 126 | (revert-buffer t t) |
8a8cdb19 DU |
127 | ;; make the buffer invisible from user |
128 | (rename-buffer (format " plstore %s" (buffer-file-name))) | |
129 | (goto-char (point-min)) | |
130 | (when (looking-at ";;; public entries\n") | |
131 | (forward-line) | |
132 | (plstore--set-alist plstore (read (point-marker))) | |
133 | (forward-sexp) | |
134 | (forward-char) | |
135 | (when (looking-at ";;; secret entries\n") | |
136 | (forward-line) | |
137 | (plstore--set-encrypted-data plstore (read (point-marker)))) | |
138 | (plstore--merge-secret plstore)))) | |
139 | ||
8977de27 DU |
140 | (defun plstore-close (plstore) |
141 | "Destroy a plstore instance PLSTORE." | |
142 | (kill-buffer (plstore--get-buffer plstore))) | |
143 | ||
144 | (defun plstore--merge-secret (plstore) | |
145 | (let ((alist (plstore--get-secret-alist plstore)) | |
146 | modified-alist | |
147 | modified-plist | |
148 | modified-entry | |
149 | entry | |
150 | plist | |
151 | placeholder) | |
152 | (plstore--set-merged-alist | |
153 | plstore | |
154 | (copy-tree (plstore--get-alist plstore))) | |
155 | (setq modified-alist (plstore--get-merged-alist plstore)) | |
156 | (while alist | |
157 | (setq entry (car alist) | |
158 | alist (cdr alist) | |
159 | plist (cdr entry) | |
160 | modified-entry (assoc (car entry) modified-alist) | |
161 | modified-plist (cdr modified-entry)) | |
162 | (while plist | |
163 | (setq placeholder | |
164 | (plist-member | |
165 | modified-plist | |
166 | (intern (concat ":secret-" | |
167 | (substring (symbol-name (car plist)) 1))))) | |
168 | (if placeholder | |
169 | (setcar placeholder (car plist))) | |
170 | (setq modified-plist | |
171 | (plist-put modified-plist (car plist) (car (cdr plist)))) | |
172 | (setq plist (nthcdr 2 plist))) | |
173 | (setcdr modified-entry modified-plist)))) | |
174 | ||
175 | (defun plstore--decrypt (plstore) | |
176 | (if (plstore--get-encrypted-data plstore) | |
177 | (let ((context (epg-make-context 'OpenPGP)) | |
178 | plain) | |
179 | (epg-context-set-passphrase-callback | |
180 | context | |
181 | (cons #'plstore-passphrase-callback-function | |
182 | plstore)) | |
183 | (epg-context-set-progress-callback | |
184 | context | |
185 | (cons #'plstore-progress-callback-function | |
186 | (format "Decrypting %s" (plstore-get-file plstore)))) | |
187 | (setq plain | |
188 | (epg-decrypt-string context | |
189 | (plstore--get-encrypted-data plstore))) | |
190 | (plstore--set-secret-alist plstore (car (read-from-string plain))) | |
191 | (plstore--merge-secret plstore) | |
192 | (plstore--set-encrypted-data plstore nil)))) | |
193 | ||
194 | (defun plstore--match (entry keys skip-if-secret-found) | |
195 | (let ((result t) key-name key-value prop-value secret-name) | |
196 | (while keys | |
197 | (setq key-name (car keys) | |
198 | key-value (car (cdr keys)) | |
199 | prop-value (plist-get (cdr entry) key-name)) | |
200 | (unless (member prop-value key-value) | |
201 | (if skip-if-secret-found | |
202 | (progn | |
203 | (setq secret-name | |
204 | (intern (concat ":secret-" | |
205 | (substring (symbol-name key-name) 1)))) | |
206 | (if (plist-member (cdr entry) secret-name) | |
207 | (setq result 'secret) | |
208 | (setq result nil | |
209 | keys nil))) | |
210 | (setq result nil | |
211 | keys nil))) | |
212 | (setq keys (nthcdr 2 keys))) | |
213 | result)) | |
214 | ||
215 | (defun plstore-find (plstore keys) | |
216 | "Perform search on PLSTORE with KEYS. | |
217 | KEYS is a plist." | |
218 | (let (entries alist entry match decrypt plist) | |
219 | ;; First, go through the merged plist alist and collect entries | |
220 | ;; matched with keys. | |
221 | (setq alist (plstore--get-merged-alist plstore)) | |
222 | (while alist | |
223 | (setq entry (car alist) | |
224 | alist (cdr alist) | |
225 | match (plstore--match entry keys t)) | |
226 | (if (eq match 'secret) | |
227 | (setq decrypt t) | |
228 | (when match | |
229 | (setq plist (cdr entry)) | |
230 | (while plist | |
231 | (if (string-match "\\`:secret-" (symbol-name (car plist))) | |
232 | (setq decrypt t | |
233 | plist nil)) | |
234 | (setq plist (nthcdr 2 plist))) | |
235 | (setq entries (cons entry entries))))) | |
236 | ;; Second, decrypt the encrypted plist and try again. | |
237 | (when decrypt | |
238 | (setq entries nil) | |
239 | (plstore--decrypt plstore) | |
240 | (setq alist (plstore--get-merged-alist plstore)) | |
241 | (while alist | |
242 | (setq entry (car alist) | |
243 | alist (cdr alist) | |
244 | match (plstore--match entry keys nil)) | |
245 | (if match | |
246 | (setq entries (cons entry entries))))) | |
247 | (nreverse entries))) | |
248 | ||
249 | (defun plstore-get (plstore name) | |
250 | "Get an entry with NAME in PLSTORE." | |
251 | (let ((entry (assoc name (plstore--get-merged-alist plstore))) | |
252 | plist) | |
253 | (setq plist (cdr entry)) | |
254 | (while plist | |
255 | (if (string-match "\\`:secret-" (symbol-name (car plist))) | |
256 | (progn | |
257 | (plstore--decrypt plstore) | |
258 | (setq entry (assoc name (plstore--get-merged-alist plstore)) | |
259 | plist nil)) | |
260 | (setq plist (nthcdr 2 plist)))) | |
261 | entry)) | |
262 | ||
263 | (defun plstore-put (plstore name keys secret-keys) | |
264 | "Put an entry with NAME in PLSTORE. | |
265 | KEYS is a plist containing non-secret data. | |
266 | SECRET-KEYS is a plist containing secret data." | |
267 | (let (entry | |
268 | plist | |
269 | secret-plist | |
270 | symbol) | |
271 | (if secret-keys | |
272 | (plstore--decrypt plstore)) | |
273 | (while secret-keys | |
274 | (setq symbol | |
275 | (intern (concat ":secret-" | |
276 | (substring (symbol-name (car secret-keys)) 1)))) | |
277 | (setq plist (plist-put plist symbol t) | |
278 | secret-plist (plist-put secret-plist | |
279 | (car secret-keys) (car (cdr secret-keys))) | |
280 | secret-keys (nthcdr 2 secret-keys))) | |
281 | (while keys | |
282 | (setq symbol | |
283 | (intern (concat ":secret-" | |
284 | (substring (symbol-name (car keys)) 1)))) | |
285 | (setq plist (plist-put plist (car keys) (car (cdr keys))) | |
286 | keys (nthcdr 2 keys))) | |
287 | (setq entry (assoc name (plstore--get-alist plstore))) | |
288 | (if entry | |
289 | (setcdr entry plist) | |
290 | (plstore--set-alist | |
291 | plstore | |
292 | (cons (cons name plist) (plstore--get-alist plstore)))) | |
293 | (when secret-plist | |
294 | (setq entry (assoc name (plstore--get-secret-alist plstore))) | |
295 | (if entry | |
296 | (setcdr entry secret-plist) | |
297 | (plstore--set-secret-alist | |
298 | plstore | |
299 | (cons (cons name secret-plist) (plstore--get-secret-alist plstore))))) | |
300 | (plstore--merge-secret plstore))) | |
301 | ||
302 | (defvar pp-escape-newlines) | |
303 | (defun plstore-save (plstore) | |
304 | "Save the contents of PLSTORE associated with a FILE." | |
305 | (with-current-buffer (plstore--get-buffer plstore) | |
306 | (erase-buffer) | |
307 | (insert ";;; public entries\n" (pp-to-string (plstore--get-alist plstore))) | |
308 | (if (plstore--get-secret-alist plstore) | |
309 | (let ((context (epg-make-context 'OpenPGP)) | |
310 | (pp-escape-newlines nil) | |
311 | cipher) | |
312 | (epg-context-set-armor context t) | |
313 | (epg-context-set-passphrase-callback | |
314 | context | |
315 | (cons #'plstore-passphrase-callback-function | |
316 | plstore)) | |
317 | (setq cipher (epg-encrypt-string context | |
318 | (pp-to-string | |
319 | (plstore--get-secret-alist plstore)) | |
320 | nil)) | |
321 | (insert ";;; secret entries\n" (pp-to-string cipher)))) | |
322 | (save-buffer))) | |
323 | ||
324 | (provide 'plstore) | |
325 | ||
326 | ;;; plstore.el ends here |