* lisp/image-mode.el (image-mode-winprops): Add winprops to
[bpt/emacs.git] / lisp / net / secrets.el
CommitLineData
7725ebb7
MA
1;;; secrets.el --- Client interface to gnome-keyring and kwallet.
2
ab422c4d 3;; Copyright (C) 2010-2013 Free Software Foundation, Inc.
7725ebb7
MA
4
5;; Author: Michael Albinus <michael.albinus@gmx.de>
6;; Keywords: comm password passphrase
7
8;; This file is part of GNU Emacs.
9
10;; GNU Emacs is free software: you can redistribute it and/or modify
11;; it under the terms of the GNU General Public License as published by
12;; the Free Software Foundation, either version 3 of the License, or
13;; (at your option) any later version.
14
15;; GNU Emacs is distributed in the hope that it will be useful,
16;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18;; GNU General Public License for more details.
19
20;; You should have received a copy of the GNU General Public License
21;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
22
23;;; Commentary:
24
25;; This package provides an implementation of the Secret Service API
26;; <http://www.freedesktop.org/wiki/Specifications/secret-storage-spec>.
27;; This API is meant to make GNOME-Keyring- and KWallet-like daemons
28;; available under a common D-BUS interface and thus increase
29;; interoperability between GNOME, KDE and other applications having
30;; the need to securely store passwords and other confidential
31;; information.
32
33;; In order to activate this package, you must add the following code
34;; into your .emacs:
7c75524e 35;;
7725ebb7 36;; (require 'secrets)
7c75524e 37;;
ae84eb97
MA
38;; Afterwards, the variable `secrets-enabled' is non-nil when there is
39;; a daemon providing this interface.
7c75524e 40
7725ebb7
MA
41;; The atomic objects to be managed by the Secret Service API are
42;; secret items, which are something an application wishes to store
43;; securely. A good example is a password that an application needs
44;; to save and use at a later date.
45
46;; Secret items are grouped in collections. A collection is similar
47;; in concept to the terms 'keyring' or 'wallet'. A common collection
48;; is called "login". A collection is stored permanently under the
49;; user's permissions, and can be accessed in a user session context.
50
51;; A collection can have an alias name. The use case for this is to
52;; set the alias "default" for a given collection, making it
53;; transparent for clients, which collection is used. Other aliases
54;; are not supported (yet). Since an alias is visible to all
55;; applications, this setting shall be performed with care.
56
57;; A list of all available collections is available by
58;;
59;; (secrets-list-collections)
60;; => ("session" "login" "ssh keys")
61
62;; The "default" alias could be set to the "login" collection by
63;;
64;; (secrets-set-alias "login" "default")
65
66;; An alias can also be dereferenced
67;;
68;; (secrets-get-alias "default")
69;; => "login"
70
71;; Collections can be created and deleted. As already said,
72;; collections are used by different applications. Therefore, those
73;; operations shall also be performed with care. Common collections,
74;; like "login", shall not be changed except adding or deleting secret
75;; items.
76;;
77;; (secrets-delete-collection "my collection")
78;; (secrets-create-collection "my collection")
79
80;; There exists a special collection called "session", which has the
44e97401 81;; lifetime of the corresponding client session (aka Emacs's
7725ebb7
MA
82;; lifetime). It is created automatically when Emacs uses the Secret
83;; Service interface, and it is deleted when Emacs is killed.
84;; Therefore, it can be used to store and retrieve secret items
85;; temporarily. This shall be preferred over creation of a persistent
86;; collection, when the information shall not live longer than Emacs.
87;; The session collection can be addressed either by the string
88;; "session", or by `nil', whenever a collection parameter is needed.
89
90;; As already said, a collection is a group of secret items. A secret
91;; item has a label, the "secret" (which is a string), and a set of
92;; lookup attributes. The attributes can be used to search and
93;; retrieve a secret item at a later date.
94
95;; A list of all available secret items of a collection is available by
96;;
97;; (secrets-list-items "my collection")
98;; => ("this item" "another item")
99
100;; Secret items can be added or deleted to a collection. In the
101;; following examples, we use the special collection "session", which
44e97401 102;; is bound to Emacs's lifetime.
7725ebb7
MA
103;;
104;; (secrets-delete-item "session" "my item")
105;; (secrets-create-item "session" "my item" "geheim"
106;; :user "joe" :host "remote-host")
107
108;; The string "geheim" is the secret of the secret item "my item".
109;; The secret string can be retrieved from items:
110;;
111;; (secrets-get-secret "session" "my item")
112;; => "geheim"
113
114;; The lookup attributes, which are specified during creation of a
115;; secret item, must be a key-value pair. Keys are keyword symbols,
116;; starting with a colon; values are strings. They can be retrieved
117;; from a given secret item:
118;;
119;; (secrets-get-attribute "session" "my item" :host)
120;; => "remote-host"
121;;
122;; (secrets-get-attributes "session" "my item")
123;; => ((:user . "joe") (:host ."remote-host"))
124
125;; The lookup attributes can be used for searching of items. If you,
126;; for example, are looking for all secret items for the user "joe",
127;; you would perform
128;;
129;; (secrets-search-items "session" :user "joe")
130;; => ("my item" "another item")
131
3a8e7cbd
MA
132;; Interactively, collections, items and their attributes could be
133;; inspected by the command `secrets-show-secrets'.
134
7725ebb7
MA
135;;; Code:
136
137;; It has been tested with GNOME Keyring 2.29.92. An implementation
138;; for KWallet will be available at
139;; svn://anonsvn.kde.org/home/kde/trunk/playground/base/ksecretservice;
140;; not tested yet.
141
142;; Pacify byte-compiler. D-Bus support in the Emacs core can be
143;; disabled with configuration option "--without-dbus". Declare used
144;; subroutines and variables of `dbus' therefore.
a464a6c7 145(eval-when-compile (require 'cl-lib))
7725ebb7 146
7725ebb7
MA
147(defvar dbus-debug)
148
149(require 'dbus)
150
8d9181c7
MA
151(autoload 'tree-widget-set-theme "tree-widget")
152(autoload 'widget-create-child-and-convert "wid-edit")
153(autoload 'widget-default-value-set "wid-edit")
154(autoload 'widget-field-end "wid-edit")
155(autoload 'widget-member "wid-edit")
3a8e7cbd
MA
156(defvar tree-widget-after-toggle-functions)
157
ae84eb97 158(defvar secrets-enabled nil
f05e1b94 159 "Whether there is a daemon offering the Secret Service API.")
ae84eb97 160
7725ebb7
MA
161(defvar secrets-debug t
162 "Write debug messages")
163
164(defconst secrets-service "org.freedesktop.secrets"
165 "The D-Bus name used to talk to Secret Service.")
166
167(defconst secrets-path "/org/freedesktop/secrets"
168 "The D-Bus root object path used to talk to Secret Service.")
169
170(defconst secrets-empty-path "/"
171 "The D-Bus object path representing an empty object.")
172
173(defsubst secrets-empty-path (path)
174 "Check, whether PATH is a valid object path.
175It returns t if not."
176 (or (not (stringp path))
177 (string-equal path secrets-empty-path)))
178
179(defconst secrets-interface-service "org.freedesktop.Secret.Service"
180 "The D-Bus interface managing sessions and collections.")
181
182;; <interface name="org.freedesktop.Secret.Service">
183;; <property name="Collections" type="ao" access="read"/>
184;; <method name="OpenSession">
185;; <arg name="algorithm" type="s" direction="in"/>
186;; <arg name="input" type="v" direction="in"/>
187;; <arg name="output" type="v" direction="out"/>
188;; <arg name="result" type="o" direction="out"/>
189;; </method>
190;; <method name="CreateCollection">
191;; <arg name="props" type="a{sv}" direction="in"/>
192;; <arg name="collection" type="o" direction="out"/>
193;; <arg name="prompt" type="o" direction="out"/>
194;; </method>
195;; <method name="SearchItems">
196;; <arg name="attributes" type="a{ss}" direction="in"/>
197;; <arg name="unlocked" type="ao" direction="out"/>
198;; <arg name="locked" type="ao" direction="out"/>
199;; </method>
200;; <method name="Unlock">
201;; <arg name="objects" type="ao" direction="in"/>
202;; <arg name="unlocked" type="ao" direction="out"/>
203;; <arg name="prompt" type="o" direction="out"/>
204;; </method>
205;; <method name="Lock">
206;; <arg name="objects" type="ao" direction="in"/>
207;; <arg name="locked" type="ao" direction="out"/>
208;; <arg name="Prompt" type="o" direction="out"/>
209;; </method>
210;; <method name="GetSecrets">
211;; <arg name="items" type="ao" direction="in"/>
212;; <arg name="session" type="o" direction="in"/>
213;; <arg name="secrets" type="a{o(oayay)}" direction="out"/>
214;; </method>
215;; <method name="ReadAlias">
216;; <arg name="name" type="s" direction="in"/>
217;; <arg name="collection" type="o" direction="out"/>
218;; </method>
219;; <method name="SetAlias">
220;; <arg name="name" type="s" direction="in"/>
221;; <arg name="collection" type="o" direction="in"/>
222;; </method>
223;; <signal name="CollectionCreated">
224;; <arg name="collection" type="o"/>
225;; </signal>
226;; <signal name="CollectionDeleted">
227;; <arg name="collection" type="o"/>
228;; </signal>
229;; </interface>
230
231(defconst secrets-interface-collection "org.freedesktop.Secret.Collection"
232 "A collection of items containing secrets.")
233
234;; <interface name="org.freedesktop.Secret.Collection">
235;; <property name="Items" type="ao" access="read"/>
236;; <property name="Label" type="s" access="readwrite"/>
237;; <property name="Locked" type="s" access="read"/>
238;; <property name="Created" type="t" access="read"/>
239;; <property name="Modified" type="t" access="read"/>
240;; <method name="Delete">
241;; <arg name="prompt" type="o" direction="out"/>
242;; </method>
243;; <method name="SearchItems">
244;; <arg name="attributes" type="a{ss}" direction="in"/>
245;; <arg name="results" type="ao" direction="out"/>
246;; </method>
247;; <method name="CreateItem">
248;; <arg name="props" type="a{sv}" direction="in"/>
249;; <arg name="secret" type="(oayay)" direction="in"/>
250;; <arg name="replace" type="b" direction="in"/>
251;; <arg name="item" type="o" direction="out"/>
252;; <arg name="prompt" type="o" direction="out"/>
253;; </method>
254;; <signal name="ItemCreated">
255;; <arg name="item" type="o"/>
256;; </signal>
257;; <signal name="ItemDeleted">
258;; <arg name="item" type="o"/>
259;; </signal>
260;; <signal name="ItemChanged">
261;; <arg name="item" type="o"/>
262;; </signal>
263;; </interface>
264
265(defconst secrets-session-collection-path
266 "/org/freedesktop/secrets/collection/session"
267 "The D-Bus temporary session collection object path.")
268
269(defconst secrets-interface-prompt "org.freedesktop.Secret.Prompt"
270 "A session tracks state between the service and a client application.")
271
272;; <interface name="org.freedesktop.Secret.Prompt">
273;; <method name="Prompt">
274;; <arg name="window-id" type="s" direction="in"/>
275;; </method>
276;; <method name="Dismiss"></method>
277;; <signal name="Completed">
278;; <arg name="dismissed" type="b"/>
279;; <arg name="result" type="v"/>
280;; </signal>
281;; </interface>
282
283(defconst secrets-interface-item "org.freedesktop.Secret.Item"
284 "A collection of items containing secrets.")
285
286;; <interface name="org.freedesktop.Secret.Item">
287;; <property name="Locked" type="b" access="read"/>
288;; <property name="Attributes" type="a{ss}" access="readwrite"/>
289;; <property name="Label" type="s" access="readwrite"/>
290;; <property name="Created" type="t" access="read"/>
291;; <property name="Modified" type="t" access="read"/>
292;; <method name="Delete">
293;; <arg name="prompt" type="o" direction="out"/>
294;; </method>
295;; <method name="GetSecret">
296;; <arg name="session" type="o" direction="in"/>
297;; <arg name="secret" type="(oayay)" direction="out"/>
298;; </method>
299;; <method name="SetSecret">
300;; <arg name="secret" type="(oayay)" direction="in"/>
301;; </method>
302;; </interface>
303;;
304;; STRUCT secret
305;; OBJECT PATH session
306;; ARRAY BYTE parameters
307;; ARRAY BYTE value
308
309(defconst secrets-interface-item-type-generic "org.freedesktop.Secret.Generic"
310 "The default item type we are using.")
311
312(defconst secrets-interface-session "org.freedesktop.Secret.Session"
313 "A session tracks state between the service and a client application.")
314
315;; <interface name="org.freedesktop.Secret.Session">
316;; <method name="Close"></method>
317;; </interface>
318
319;;; Sessions.
320
321(defvar secrets-session-path secrets-empty-path
322 "The D-Bus session path of the active session.
323A session path `secrets-empty-path' indicates there is no open session.")
324
325(defun secrets-close-session ()
326 "Close the secret service session, if any."
327 (dbus-ignore-errors
328 (dbus-call-method
329 :session secrets-service secrets-session-path
330 secrets-interface-session "Close"))
331 (setq secrets-session-path secrets-empty-path))
332
333(defun secrets-open-session (&optional reopen)
334 "Open a new session with \"plain\" algorithm.
335If there exists another active session, and REOPEN is nil, that
336session will be used. The object path of the session will be
337returned, and it will be stored in `secrets-session-path'."
338 (when reopen (secrets-close-session))
339 (when (secrets-empty-path secrets-session-path)
340 (setq secrets-session-path
341 (cadr
342 (dbus-call-method
343 :session secrets-service secrets-path
344 secrets-interface-service "OpenSession" "plain" '(:variant "")))))
345 (when secrets-debug
346 (message "Secret Service session: %s" secrets-session-path))
347 secrets-session-path)
348
349;;; Prompts.
350
351(defvar secrets-prompt-signal nil
352 "Internal variable to catch signals from `secrets-interface-prompt'.")
353
354(defun secrets-prompt (prompt)
355 "Handle the prompt identified by object path PROMPT."
356 (unless (secrets-empty-path prompt)
357 (let ((object
358 (dbus-register-signal
359 :session secrets-service prompt
360 secrets-interface-prompt "Completed" 'secrets-prompt-handler)))
361 (dbus-call-method
362 :session secrets-service prompt
363 secrets-interface-prompt "Prompt" (frame-parameter nil 'window-id))
364 (unwind-protect
365 (progn
366 ;; Wait until the returned prompt signal has put the
367 ;; result into `secrets-prompt-signal'.
368 (while (null secrets-prompt-signal)
369 (read-event nil nil 0.1))
370 ;; Return the object(s). It is a variant, so we must use a car.
371 (car secrets-prompt-signal))
372 ;; Cleanup.
373 (setq secrets-prompt-signal nil)
374 (dbus-unregister-object object)))))
375
376(defun secrets-prompt-handler (&rest args)
377 "Handler for signals emitted by `secrets-interface-prompt'."
378 ;; An empty object path is always identified as `secrets-empty-path'
c7015153 379 ;; or `nil'. Either we set it explicitly, or it is returned by the
7725ebb7
MA
380 ;; "Completed" signal.
381 (if (car args) ;; dismissed
382 (setq secrets-prompt-signal (list secrets-empty-path))
383 (setq secrets-prompt-signal (cadr args))))
384
385;;; Collections.
386
387(defvar secrets-collection-paths nil
388 "Cached D-Bus object paths of available collections.")
389
390(defun secrets-collection-handler (&rest args)
391 "Handler for signals emitted by `secrets-interface-service'."
392 (cond
393 ((string-equal (dbus-event-member-name last-input-event) "CollectionCreated")
394 (add-to-list 'secrets-collection-paths (car args)))
395 ((string-equal (dbus-event-member-name last-input-event) "CollectionDeleted")
396 (setq secrets-collection-paths
397 (delete (car args) secrets-collection-paths)))))
398
7725ebb7
MA
399(defun secrets-get-collections ()
400 "Return the object paths of all available collections."
401 (setq secrets-collection-paths
402 (or secrets-collection-paths
403 (dbus-get-property
404 :session secrets-service secrets-path
405 secrets-interface-service "Collections"))))
406
407(defun secrets-get-collection-properties (collection-path)
408 "Return all properties of collection identified by COLLECTION-PATH."
409 (unless (secrets-empty-path collection-path)
410 (dbus-get-all-properties
411 :session secrets-service collection-path
412 secrets-interface-collection)))
413
414(defun secrets-get-collection-property (collection-path property)
415 "Return property PROPERTY of collection identified by COLLECTION-PATH."
416 (unless (or (secrets-empty-path collection-path) (not (stringp property)))
417 (dbus-get-property
418 :session secrets-service collection-path
419 secrets-interface-collection property)))
420
421(defun secrets-list-collections ()
422 "Return a list of collection names."
423 (mapcar
424 (lambda (collection-path)
425 (if (string-equal collection-path secrets-session-collection-path)
426 "session"
427 (secrets-get-collection-property collection-path "Label")))
428 (secrets-get-collections)))
429
430(defun secrets-collection-path (collection)
09e80d9f 431 "Return the object path of collection labeled COLLECTION.
7725ebb7
MA
432If COLLECTION is nil, return the session collection path.
433If there is no such COLLECTION, return nil."
434 (or
435 ;; The "session" collection.
436 (if (or (null collection) (string-equal "session" collection))
437 secrets-session-collection-path)
438 ;; Check for an alias.
439 (let ((collection-path
440 (dbus-call-method
441 :session secrets-service secrets-path
442 secrets-interface-service "ReadAlias" collection)))
443 (unless (secrets-empty-path collection-path)
444 collection-path))
445 ;; Check the collections.
446 (catch 'collection-found
447 (dolist (collection-path (secrets-get-collections) nil)
4c145d5d
MA
448 (when (string-equal
449 collection
450 (secrets-get-collection-property collection-path "Label"))
7725ebb7
MA
451 (throw 'collection-found collection-path))))))
452
453(defun secrets-create-collection (collection)
09e80d9f 454 "Create collection labeled COLLECTION if it doesn't exist.
7725ebb7
MA
455Return the D-Bus object path for collection."
456 (let ((collection-path (secrets-collection-path collection)))
457 ;; Create the collection.
458 (when (secrets-empty-path collection-path)
459 (setq collection-path
460 (secrets-prompt
461 (cadr
462 ;; "CreateCollection" returns the prompt path as second arg.
463 (dbus-call-method
464 :session secrets-service secrets-path
465 secrets-interface-service "CreateCollection"
466 `(:array (:dict-entry "Label" (:variant ,collection))))))))
467 ;; Return object path of the collection.
468 collection-path))
469
470(defun secrets-get-alias (alias)
471 "Return the collection name ALIAS is referencing to.
472For the time being, only the alias \"default\" is supported."
473 (secrets-get-collection-property
474 (dbus-call-method
475 :session secrets-service secrets-path
476 secrets-interface-service "ReadAlias" alias)
477 "Label"))
478
479(defun secrets-set-alias (collection alias)
09e80d9f 480 "Set ALIAS as alias of collection labeled COLLECTION.
7725ebb7
MA
481For the time being, only the alias \"default\" is supported."
482 (let ((collection-path (secrets-collection-path collection)))
483 (unless (secrets-empty-path collection-path)
484 (dbus-call-method
485 :session secrets-service secrets-path
486 secrets-interface-service "SetAlias"
487 alias :object-path collection-path))))
488
4c145d5d
MA
489(defun secrets-delete-alias (alias)
490 "Delete ALIAS, referencing to a collection."
491 (dbus-call-method
492 :session secrets-service secrets-path
493 secrets-interface-service "SetAlias"
494 alias :object-path secrets-empty-path))
495
7725ebb7 496(defun secrets-unlock-collection (collection)
09e80d9f 497 "Unlock collection labeled COLLECTION.
7725ebb7
MA
498If successful, return the object path of the collection."
499 (let ((collection-path (secrets-collection-path collection)))
500 (unless (secrets-empty-path collection-path)
501 (secrets-prompt
502 (cadr
503 (dbus-call-method
504 :session secrets-service secrets-path secrets-interface-service
505 "Unlock" `(:array :object-path ,collection-path)))))
506 collection-path))
507
508(defun secrets-delete-collection (collection)
09e80d9f 509 "Delete collection labeled COLLECTION."
7725ebb7
MA
510 (let ((collection-path (secrets-collection-path collection)))
511 (unless (secrets-empty-path collection-path)
512 (secrets-prompt
513 (dbus-call-method
514 :session secrets-service collection-path
515 secrets-interface-collection "Delete")))))
516
517;;; Items.
518
519(defun secrets-get-items (collection-path)
520 "Return the object paths of all available items in COLLECTION-PATH."
521 (unless (secrets-empty-path collection-path)
522 (secrets-open-session)
523 (dbus-get-property
524 :session secrets-service collection-path
525 secrets-interface-collection "Items")))
526
527(defun secrets-get-item-properties (item-path)
528 "Return all properties of item identified by ITEM-PATH."
529 (unless (secrets-empty-path item-path)
530 (dbus-get-all-properties
531 :session secrets-service item-path
532 secrets-interface-item)))
533
534(defun secrets-get-item-property (item-path property)
535 "Return property PROPERTY of item identified by ITEM-PATH."
536 (unless (or (secrets-empty-path item-path) (not (stringp property)))
537 (dbus-get-property
538 :session secrets-service item-path
539 secrets-interface-item property)))
540
541(defun secrets-list-items (collection)
542 "Return a list of all item labels of COLLECTION."
543 (let ((collection-path (secrets-unlock-collection collection)))
544 (unless (secrets-empty-path collection-path)
545 (mapcar
546 (lambda (item-path)
547 (secrets-get-item-property item-path "Label"))
548 (secrets-get-items collection-path)))))
549
550(defun secrets-search-items (collection &rest attributes)
551 "Search items in COLLECTION with ATTRIBUTES.
552ATTRIBUTES are key-value pairs. The keys are keyword symbols,
553starting with a colon. Example:
554
555 \(secrets-create-item \"Tramp collection\" \"item\" \"geheim\"
556 :method \"sudo\" :user \"joe\" :host \"remote-host\"\)
557
558The object paths of the found items are returned as list."
559 (let ((collection-path (secrets-unlock-collection collection))
560 result props)
561 (unless (secrets-empty-path collection-path)
562 ;; Create attributes list.
563 (while (consp (cdr attributes))
564 (unless (keywordp (car attributes))
565 (error 'wrong-type-argument (car attributes)))
566 (setq props (add-to-list
567 'props
568 (list :dict-entry
8d9181c7 569 (substring (symbol-name (car attributes)) 1)
7725ebb7
MA
570 (cadr attributes))
571 'append)
572 attributes (cddr attributes)))
573 ;; Search. The result is a list of two lists, the object paths
574 ;; of the unlocked and the locked items.
575 (setq result
576 (dbus-call-method
577 :session secrets-service collection-path
578 secrets-interface-collection "SearchItems"
579 (if props
580 (cons :array props)
581 '(:array :signature "{ss}"))))
582 ;; Return the found items.
583 (mapcar
584 (lambda (item-path) (secrets-get-item-property item-path "Label"))
585 (append (car result) (cadr result))))))
586
587(defun secrets-create-item (collection item password &rest attributes)
588 "Create a new item in COLLECTION with label ITEM and password PASSWORD.
589ATTRIBUTES are key-value pairs set for the created item. The
590keys are keyword symbols, starting with a colon. Example:
591
592 \(secrets-create-item \"Tramp collection\" \"item\" \"geheim\"
593 :method \"sudo\" :user \"joe\" :host \"remote-host\"\)
594
595The object path of the created item is returned."
596 (unless (member item (secrets-list-items collection))
597 (let ((collection-path (secrets-unlock-collection collection))
598 result props)
599 (unless (secrets-empty-path collection-path)
600 ;; Create attributes list.
601 (while (consp (cdr attributes))
602 (unless (keywordp (car attributes))
603 (error 'wrong-type-argument (car attributes)))
604 (setq props (add-to-list
605 'props
606 (list :dict-entry
8d9181c7 607 (substring (symbol-name (car attributes)) 1)
7725ebb7
MA
608 (cadr attributes))
609 'append)
610 attributes (cddr attributes)))
611 ;; Create the item.
612 (setq result
613 (dbus-call-method
614 :session secrets-service collection-path
615 secrets-interface-collection "CreateItem"
616 ;; Properties.
617 (append
618 `(:array
619 (:dict-entry "Label" (:variant ,item))
620 (:dict-entry
621 "Type" (:variant ,secrets-interface-item-type-generic)))
622 (when props
623 `((:dict-entry
624 "Attributes" (:variant ,(append '(:array) props))))))
625 ;; Secret.
626 `(:struct :object-path ,secrets-session-path
627 (:array :signature "y") ;; no parameters.
628 ,(dbus-string-to-byte-array password))
629 ;; Do not replace. Replace does not seem to work.
630 nil))
631 (secrets-prompt (cadr result))
632 ;; Return the object path.
633 (car result)))))
634
635(defun secrets-item-path (collection item)
09e80d9f 636 "Return the object path of item labeled ITEM in COLLECTION.
7725ebb7
MA
637If there is no such item, return nil."
638 (let ((collection-path (secrets-unlock-collection collection)))
639 (catch 'item-found
640 (dolist (item-path (secrets-get-items collection-path))
641 (when (string-equal item (secrets-get-item-property item-path "Label"))
642 (throw 'item-found item-path))))))
643
644(defun secrets-get-secret (collection item)
09e80d9f 645 "Return the secret of item labeled ITEM in COLLECTION.
7725ebb7
MA
646If there is no such item, return nil."
647 (let ((item-path (secrets-item-path collection item)))
648 (unless (secrets-empty-path item-path)
649 (dbus-byte-array-to-string
a464a6c7 650 (cl-caddr
7725ebb7
MA
651 (dbus-call-method
652 :session secrets-service item-path secrets-interface-item
653 "GetSecret" :object-path secrets-session-path))))))
654
655(defun secrets-get-attributes (collection item)
09e80d9f 656 "Return the lookup attributes of item labeled ITEM in COLLECTION.
7725ebb7
MA
657If there is no such item, or the item has no attributes, return nil."
658 (unless (stringp collection) (setq collection "default"))
659 (let ((item-path (secrets-item-path collection item)))
660 (unless (secrets-empty-path item-path)
661 (mapcar
8d9181c7
MA
662 (lambda (attribute)
663 (cons (intern (concat ":" (car attribute))) (cadr attribute)))
7725ebb7
MA
664 (dbus-get-property
665 :session secrets-service item-path
666 secrets-interface-item "Attributes")))))
667
668(defun secrets-get-attribute (collection item attribute)
09e80d9f 669 "Return the value of ATTRIBUTE of item labeled ITEM in COLLECTION.
7725ebb7
MA
670If there is no such item, or the item doesn't own this attribute, return nil."
671 (cdr (assoc attribute (secrets-get-attributes collection item))))
672
673(defun secrets-delete-item (collection item)
674 "Delete ITEM in COLLECTION."
675 (let ((item-path (secrets-item-path collection item)))
676 (unless (secrets-empty-path item-path)
677 (secrets-prompt
678 (dbus-call-method
679 :session secrets-service item-path
680 secrets-interface-item "Delete")))))
681
3a8e7cbd
MA
682;;; Visualization.
683
684(define-derived-mode secrets-mode nil "Secrets"
8d9181c7 685 "Major mode for presenting password entries retrieved by Security Service.
3a8e7cbd
MA
686In this mode, widgets represent the search results.
687
8d9181c7 688\\{secrets-mode-map}"
3a8e7cbd
MA
689 ;; Keymap.
690 (setq secrets-mode-map (copy-keymap special-mode-map))
691 (set-keymap-parent secrets-mode-map widget-keymap)
692 (define-key secrets-mode-map "z" 'kill-this-buffer)
693
694 ;; When we toggle, we must set temporary widgets.
695 (set (make-local-variable 'tree-widget-after-toggle-functions)
696 '(secrets-tree-widget-after-toggle-function))
697
698 (when (not (called-interactively-p 'interactive))
699 ;; Initialize buffer.
700 (setq buffer-read-only t)
701 (let ((inhibit-read-only t))
702 (erase-buffer))))
703
704;; It doesn't make sense to call it interactively.
705(put 'secrets-mode 'disabled t)
706
707;; The very first buffer created with `secrets-mode' does not have the
708;; keymap etc. So we create a dummy buffer. Stupid.
709(with-temp-buffer (secrets-mode))
710
8d9181c7
MA
711;; We autoload `secrets-show-secrets' only on systems with D-Bus support.
712;;;###autoload(when (featurep 'dbusbind)
713;;;###autoload (autoload 'secrets-show-secrets "secrets" nil t))
714
3a8e7cbd
MA
715(defun secrets-show-secrets ()
716 "Display a list of collections from the Secret Service API.
717The collections are in tree view, that means they can be expanded
718to the corresponding secret items, which could also be expanded
719to their attributes."
720 (interactive)
8d9181c7
MA
721
722 ;; Check, whether the Secret Service API is enabled.
723 (if (null secrets-enabled)
724 (message "Secret Service not available")
725
726 ;; Create the search buffer.
727 (with-current-buffer (get-buffer-create "*Secrets*")
728 (switch-to-buffer-other-window (current-buffer))
e4920bc9 729 ;; Initialize buffer with `secrets-mode'.
8d9181c7
MA
730 (secrets-mode)
731 (secrets-show-collections))))
3a8e7cbd
MA
732
733(defun secrets-show-collections ()
734 "Show all available collections."
735 (let ((inhibit-read-only t)
736 (alias (secrets-get-alias "default")))
737 (erase-buffer)
738 (tree-widget-set-theme "folder")
739 (dolist (coll (secrets-list-collections))
740 (widget-create
741 `(tree-widget
742 :tag ,coll
743 :collection ,coll
744 :open nil
745 :sample-face bold
746 :expander secrets-expand-collection)))))
747
748(defun secrets-expand-collection (widget)
749 "Expand items of collection shown as WIDGET."
750 (let ((coll (widget-get widget :collection)))
751 (mapcar
752 (lambda (item)
753 `(tree-widget
754 :tag ,item
755 :collection ,coll
756 :item ,item
757 :open nil
758 :sample-face bold
759 :expander secrets-expand-item))
760 (secrets-list-items coll))))
761
762(defun secrets-expand-item (widget)
763 "Expand password and attributes of item shown as WIDGET."
764 (let* ((coll (widget-get widget :collection))
765 (item (widget-get widget :item))
766 (attributes (secrets-get-attributes coll item))
767 ;; padding is needed to format attribute names.
768 (padding
8d9181c7
MA
769 (apply
770 'max
771 (cons
772 (1+ (length "password"))
773 (mapcar
fe3c5669 774 ;; Attribute names have a leading ":", which will be suppressed.
8d9181c7
MA
775 (lambda (attribute) (length (symbol-name (car attribute))))
776 attributes)))))
3a8e7cbd
MA
777 (cons
778 ;; The password widget.
779 `(editable-field :tag "password"
780 :secret ?*
781 :value ,(secrets-get-secret coll item)
782 :sample-face widget-button-pressed
783 ;; We specify :size in order to limit the field.
784 :size 0
785 :format ,(concat
786 "%{%t%}:"
787 (make-string (- padding (length "password")) ? )
788 "%v\n"))
789 (mapcar
790 (lambda (attribute)
8d9181c7 791 (let ((name (substring (symbol-name (car attribute)) 1))
3a8e7cbd
MA
792 (value (cdr attribute)))
793 ;; The attribute widget.
794 `(editable-field :tag ,name
795 :value ,value
796 :sample-face widget-documentation
797 ;; We specify :size in order to limit the field.
798 :size 0
799 :format ,(concat
800 "%{%t%}:"
801 (make-string (- padding (length name)) ? )
802 "%v\n"))))
803 attributes))))
804
805(defun secrets-tree-widget-after-toggle-function (widget &rest ignore)
806 "Add a temporary widget to show the password."
807 (dolist (child (widget-get widget :children))
808 (when (widget-member child :secret)
809 (goto-char (widget-field-end child))
810 (widget-insert " ")
811 (widget-create-child-and-convert
812 child 'push-button
813 :notify 'secrets-tree-widget-show-password
814 "Show password")))
815 (widget-setup))
816
817(defun secrets-tree-widget-show-password (widget &rest ignore)
818 "Show password, and remove temporary widget."
819 (let ((parent (widget-get widget :parent)))
820 (widget-put parent :secret nil)
821 (widget-default-value-set parent (widget-get parent :value))
822 (widget-setup)))
823
824;;; Initialization.
825
ae84eb97
MA
826(when (dbus-ping :session secrets-service 100)
827
828 ;; We must reset all variables, when there is a new instance of the
829 ;; "org.freedesktop.secrets" service.
830 (dbus-register-signal
831 :session dbus-service-dbus dbus-path-dbus
832 dbus-interface-dbus "NameOwnerChanged"
833 (lambda (&rest args)
834 (when secrets-debug (message "Secret Service has changed: %S" args))
835 (setq secrets-session-path secrets-empty-path
836 secrets-prompt-signal nil
837 secrets-collection-paths nil))
838 secrets-service)
839
840 ;; We want to refresh our cache, when there is a change in
841 ;; collections.
842 (dbus-register-signal
843 :session secrets-service secrets-path
844 secrets-interface-service "CollectionCreated"
845 'secrets-collection-handler)
846
847 (dbus-register-signal
848 :session secrets-service secrets-path
849 secrets-interface-service "CollectionDeleted"
850 'secrets-collection-handler)
851
852 ;; We shall inform, whether the secret service is enabled on this
853 ;; machine.
f05e1b94 854 (setq secrets-enabled t))
ae84eb97
MA
855
856(provide 'secrets)
7725ebb7
MA
857
858;;; TODO:
859
860;; * secrets-debug should be structured like auth-source-debug to
861;; prevent leaking sensitive information. Right now I don't see
862;; anything sensitive though.
863;; * Check, whether the dh-ietf1024-aes128-cbc-pkcs7 algorithm can be
864;; used for the transfer of the secrets. Currently, we use the
865;; plain algorithm.