plstore.el 14 KB

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