summaryrefslogtreecommitdiff
path: root/lisp/net/secrets.el
blob: ad271679618dfaa8b6bd21368a2c5223985f0bc8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
;;; secrets.el --- Client interface to gnome-keyring and kwallet. -*- lexical-binding: t -*-

;; Copyright (C) 2010-2021 Free Software Foundation, Inc.

;; Author: Michael Albinus <michael.albinus@gmx.de>
;; Keywords: comm password passphrase

;; This file is part of GNU Emacs.

;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; This package provides an implementation of the Secret Service API
;; <https://www.freedesktop.org/wiki/Specifications/secret-storage-spec>.
;; This API is meant to make GNOME-Keyring- and KWallet-like daemons
;; available under a common D-BUS interface and thus increase
;; interoperability between GNOME, KDE and other applications having
;; the need to securely store passwords and other confidential
;; information.

;; In order to activate this package, you must add the following code
;; into your .emacs:
;;
;;   (require 'secrets)
;;
;; Afterwards, the variable `secrets-enabled' is non-nil when there is
;; a daemon providing this interface.

;; The atomic objects to be managed by the Secret Service API are
;; secret items, which are something an application wishes to store
;; securely.  A good example is a password that an application needs
;; to save and use at a later date.

;; Secret items are grouped in collections.  A collection is similar
;; in concept to the terms 'keyring' or 'wallet'.  A common collection
;; is called "login".  A collection is stored permanently under the
;; user's permissions, and can be accessed in a user session context.

;; A collection can have an alias name.  The use case for this is to
;; set the alias "default" for a given collection, making it
;; transparent for clients, which collection is used.  Other aliases
;; are not supported (yet).  Since an alias is visible to all
;; applications, this setting shall be performed with care.

;; A list of all available collections is available by
;;
;;   (secrets-list-collections)
;;    => ("session" "login" "ssh keys")

;; The "default" alias could be set to the "login" collection by
;;
;;   (secrets-set-alias "login" "default")

;; An alias can also be dereferenced
;;
;;   (secrets-get-alias "default")
;;    => "login"

;; Collections can be created and deleted.  As already said,
;; collections are used by different applications.  Therefore, those
;; operations shall also be performed with care.  Common collections,
;; like "login", shall not be changed except adding or deleting secret
;; items.
;;
;;   (secrets-delete-collection "my collection")
;;   (secrets-create-collection "my collection")

;; There exists a special collection called "session", which has the
;; lifetime of the corresponding client session (aka Emacs's
;; lifetime).  It is created automatically when Emacs uses the Secret
;; Service interface, and it is deleted when Emacs is killed.
;; Therefore, it can be used to store and retrieve secret items
;; temporarily.  This shall be preferred over creation of a persistent
;; collection, when the information shall not live longer than Emacs.
;; The session collection can be addressed either by the string
;; "session", or by nil, whenever a collection parameter is needed.

;; As already said, a collection is a group of secret items.  A secret
;; item has a label, the "secret" (which is a string), and a set of
;; lookup attributes.  The attributes can be used to search and
;; retrieve a secret item at a later date.

;; A list of all available secret items of a collection is available by
;;
;;   (secrets-list-items "my collection")
;;    => ("this item" "another item")

;; Secret items can be added or deleted to a collection.  In the
;; following examples, we use the special collection "session", which
;; is bound to Emacs's lifetime.
;;
;;   (secrets-delete-item "session" "my item")
;;   (secrets-create-item "session" "my item" "geheim"
;;                        :user "joe" :host "remote-host")

;; The string "geheim" is the secret of the secret item "my item".
;; The secret string can be retrieved from items:
;;
;;   (secrets-get-secret "session" "my item")
;;    => "geheim"

;; The lookup attributes, which are specified during creation of a
;; secret item, must be a key-value pair.  Keys are keyword symbols,
;; starting with a colon; values are strings.  They can be retrieved
;; from a given secret item:
;;
;;   (secrets-get-attribute "session" "my item" :host)
;;    => "remote-host"
;;
;;   (secrets-get-attributes "session" "my item")
;;    => ((:user . "joe") (:host ."remote-host"))

;; The lookup attributes can be used for searching of items.  If you,
;; for example, are looking for all secret items for the user "joe",
;; you would perform
;;
;;   (secrets-search-items "session" :user "joe")
;;    => ("my item" "another item")

;; Interactively, collections, items and their attributes could be
;; inspected by the command `secrets-show-secrets'.

;;; Code:

;; It has been tested with GNOME Keyring 2.29.92.  An implementation
;; for KWallet will be available at
;; svn://anonsvn.kde.org/home/kde/trunk/playground/base/ksecretservice;
;; not tested yet.

;; Pacify byte-compiler.  D-Bus support in the Emacs core can be
;; disabled with configuration option "--without-dbus".  Declare used
;; subroutines and variables of `dbus' therefore.
(eval-when-compile (require 'cl-lib))

(defvar dbus-debug)

(require 'dbus)

(autoload 'tree-widget-set-theme "tree-widget")
(autoload 'widget-create-child-and-convert "wid-edit")
(autoload 'widget-default-value-set "wid-edit")
(autoload 'widget-field-end "wid-edit")
(autoload 'widget-member "wid-edit")
(defvar tree-widget-after-toggle-functions)

(defvar secrets-enabled nil
  "Whether there is a daemon offering the Secret Service API.")

(defvar secrets-debug nil
  "Write debug messages")

(defconst secrets-service "org.freedesktop.secrets"
  "The D-Bus name used to talk to Secret Service.")

(defconst secrets-path "/org/freedesktop/secrets"
  "The D-Bus root object path used to talk to Secret Service.")

(defconst secrets-empty-path "/"
  "The D-Bus object path representing an empty object.")

(defsubst secrets-empty-path (path)
  "Check, whether PATH is a valid object path.
It returns t if not."
  (or (not (stringp path))
      (string-equal path secrets-empty-path)))

(defconst secrets-interface-service "org.freedesktop.Secret.Service"
  "The D-Bus interface managing sessions and collections.")

;; <interface name="org.freedesktop.Secret.Service">
;;   <property name="Collections" type="ao" access="read"/>
;;   <method name="OpenSession">
;;     <arg name="algorithm" type="s" direction="in"/>
;;     <arg name="input"     type="v" direction="in"/>
;;     <arg name="output"    type="v" direction="out"/>
;;     <arg name="result"    type="o" direction="out"/>
;;   </method>
;;   <method name="CreateCollection">
;;     <arg name="props"      type="a{sv}" direction="in"/>
;;     <arg name="alias"      type="s"     direction="in"/>   ;; Added 2011/3/1
;;     <arg name="collection" type="o"     direction="out"/>
;;     <arg name="prompt"     type="o"     direction="out"/>
;;   </method>
;;   <method name="SearchItems">
;;     <arg name="attributes" type="a{ss}" direction="in"/>
;;     <arg name="unlocked"   type="ao"    direction="out"/>
;;     <arg name="locked"     type="ao"    direction="out"/>
;;   </method>
;;   <method name="Unlock">
;;     <arg name="objects"  type="ao" direction="in"/>
;;     <arg name="unlocked" type="ao" direction="out"/>
;;     <arg name="prompt"   type="o"  direction="out"/>
;;   </method>
;;   <method name="Lock">
;;     <arg name="objects" type="ao" direction="in"/>
;;     <arg name="locked"  type="ao" direction="out"/>
;;     <arg name="Prompt"  type="o"  direction="out"/>
;;   </method>
;;   <method name="GetSecrets">
;;     <arg name="items"   type="ao"           direction="in"/>
;;     <arg name="session" type="o"            direction="in"/>
;;     <arg name="secrets" type="a{o(oayays)}" direction="out"/>
;;   </method>
;;   <method name="ReadAlias">
;;     <arg name="name"       type="s" direction="in"/>
;;     <arg name="collection" type="o" direction="out"/>
;;   </method>
;;   <method name="SetAlias">
;;     <arg name="name"       type="s" direction="in"/>
;;     <arg name="collection" type="o" direction="in"/>
;;   </method>
;;   <signal name="CollectionCreated">
;;     <arg name="collection" type="o"/>
;;   </signal>
;;   <signal name="CollectionDeleted">
;;     <arg name="collection" type="o"/>
;;   </signal>
;; </interface>

(defconst secrets-interface-collection "org.freedesktop.Secret.Collection"
  "A collection of items containing secrets.")

;; <interface name="org.freedesktop.Secret.Collection">
;;   <property name="Items"    type="ao" access="read"/>
;;   <property name="Label"    type="s"  access="readwrite"/>
;;   <property name="Locked"   type="b"  access="read"/>
;;   <property name="Created"  type="t"  access="read"/>
;;   <property name="Modified" type="t"  access="read"/>
;;   <method name="Delete">
;;     <arg name="prompt" type="o" direction="out"/>
;;   </method>
;;   <method name="SearchItems">
;;     <arg name="attributes" type="a{ss}" direction="in"/>
;;     <arg name="results"    type="ao"    direction="out"/>
;;   </method>
;;   <method name="CreateItem">
;;     <arg name="props"   type="a{sv}"    direction="in"/>
;;     <arg name="secret"  type="(oayays)" direction="in"/>
;;     <arg name="replace" type="b"        direction="in"/>
;;     <arg name="item"    type="o"        direction="out"/>
;;     <arg name="prompt"  type="o"        direction="out"/>
;;   </method>
;;   <signal name="ItemCreated">
;;     <arg name="item" type="o"/>
;;   </signal>
;;   <signal name="ItemDeleted">
;;     <arg name="item" type="o"/>
;;   </signal>
;;   <signal name="ItemChanged">
;;     <arg name="item" type="o"/>
;;   </signal>
;; </interface>

(defconst secrets-session-collection-path
  "/org/freedesktop/secrets/collection/session"
  "The D-Bus temporary session collection object path.")

(defconst secrets-interface-prompt "org.freedesktop.Secret.Prompt"
  "A session tracks state between the service and a client application.")

;; <interface name="org.freedesktop.Secret.Prompt">
;;   <method name="Prompt">
;;     <arg name="window-id" type="s" direction="in"/>
;;   </method>
;;   <method name="Dismiss"></method>
;;   <signal name="Completed">
;;     <arg name="dismissed" type="b"/>
;;     <arg name="result"    type="v"/>
;;   </signal>
;; </interface>

(defconst secrets-interface-item "org.freedesktop.Secret.Item"
  "A collection of items containing secrets.")

;; <interface name="org.freedesktop.Secret.Item">
;;   <property name="Locked"     type="b"     access="read"/>
;;   <property name="Attributes" type="a{ss}" access="readwrite"/>
;;   <property name="Label"      type="s"     access="readwrite"/>
;;   <property name="Created"    type="t"     access="read"/>
;;   <property name="Modified"   type="t"     access="read"/>
;;   <method name="Delete">
;;     <arg name="prompt" type="o" direction="out"/>
;;   </method>
;;   <method name="GetSecret">
;;     <arg name="session" type="o"        direction="in"/>
;;     <arg name="secret"  type="(oayays)" direction="out"/>
;;   </method>
;;   <method name="SetSecret">
;;     <arg name="secret" type="(oayays)" direction="in"/>
;;   </method>
;; </interface>
;;
;; STRUCT	secret
;;   OBJECT PATH  session
;;   ARRAY BYTE	  parameters
;;   ARRAY BYTE	  value
;;   STRING	  content_type     ;; Added 2011/2/9

(defconst secrets-interface-item-type-generic "org.freedesktop.Secret.Generic"
  "The default item type we are using.")

;; We cannot use introspection, because some servers, like
;; mate-keyring-daemon, don't provide relevant data.  Once the dust
;; has settled, we shall assume the new interface, and get rid of the test.
(defconst secrets-struct-secret-content-type
  (ignore-errors
    (let ((content-type "text/plain")
	  (path (cadr
		 (dbus-call-method
		  :session secrets-service secrets-path
		  secrets-interface-service
		  "OpenSession" "plain" '(:variant ""))))
	  result)
      ;; Create a dummy item.
      (setq result
	    (dbus-call-method
	     :session secrets-service secrets-session-collection-path
	     secrets-interface-collection "CreateItem"
	     ;; Properties.
	     `(:array
	       (:dict-entry ,(concat secrets-interface-item ".Label")
			    (:variant " ")))
	     ;; Secret.
	     `(:struct :object-path ,path
		       (:array :signature "y")
		       ,(dbus-string-to-byte-array " ")
		       :string ,content-type)
	     ;; Don't replace.
	     nil))
      ;; Remove it.
      (dbus-call-method
       :session secrets-service (car result)
       secrets-interface-item "Delete")
      ;; Result.
      `(,content-type)))
  "The content_type of a secret struct.
It must be wrapped as list, because we add it via `append'.  This
is an interface introduced in 2011.")

(defconst secrets-interface-session "org.freedesktop.Secret.Session"
  "A session tracks state between the service and a client application.")

;; <interface name="org.freedesktop.Secret.Session">
;;   <method name="Close"></method>
;; </interface>

;;; Sessions.

(defvar secrets-session-path secrets-empty-path
  "The D-Bus session path of the active session.
A session path `secrets-empty-path' indicates there is no open session.")

(defun secrets-close-session ()
  "Close the secret service session, if any."
  (dbus-ignore-errors
    (dbus-call-method
     :session secrets-service secrets-session-path
     secrets-interface-session "Close"))
  (setq secrets-session-path secrets-empty-path))

(defun secrets-open-session (&optional reopen)
  "Open a new session with \"plain\" algorithm.
If there exists another active session, and REOPEN is nil, that
session will be used.  The object path of the session will be
returned, and it will be stored in `secrets-session-path'."
  (when reopen (secrets-close-session))
  (when (secrets-empty-path secrets-session-path)
    (setq secrets-session-path
	  (cadr
	   (dbus-call-method
	    :session secrets-service secrets-path
	    secrets-interface-service "OpenSession" "plain" '(:variant "")))))
  (when secrets-debug
    (message "Secret Service session: %s" secrets-session-path))
  secrets-session-path)

;;; Prompts.

(defvar secrets-prompt-signal nil
  "Internal variable to catch signals from `secrets-interface-prompt'.")

(defun secrets-prompt (prompt)
  "Handle the prompt identified by object path PROMPT."
  (unless (secrets-empty-path prompt)
    (let ((object
	   (dbus-register-signal
	    :session secrets-service prompt
	    secrets-interface-prompt "Completed" 'secrets-prompt-handler)))
      (dbus-call-method
       :session secrets-service prompt
       secrets-interface-prompt "Prompt" (frame-parameter nil 'window-id))
      (unwind-protect
	  (progn
	    ;; Wait until the returned prompt signal has put the
	    ;; result into `secrets-prompt-signal'.
	    (while (null secrets-prompt-signal)
	      (read-event nil nil 0.1))
	    ;; Return the object(s).  It is a variant, so we must use a car.
	    (car secrets-prompt-signal))
	;; Cleanup.
	(setq secrets-prompt-signal nil)
	(dbus-unregister-object object)))))

(defun secrets-prompt-handler (&rest args)
  "Handler for signals emitted by `secrets-interface-prompt'."
  ;; An empty object path is always identified as `secrets-empty-path'
  ;; or nil.  Either we set it explicitly, or it is returned by the
  ;; "Completed" signal.
  (if (car args) ;; dismissed
      (setq secrets-prompt-signal (list secrets-empty-path))
    (setq secrets-prompt-signal (cadr args))))

;;; Collections.

(defvar secrets-collection-paths nil
  "Cached D-Bus object paths of available collections.")

(defun secrets-collection-handler (&rest args)
  "Handler for signals emitted by `secrets-interface-service'."
  (cond
   ((string-equal (dbus-event-member-name last-input-event) "CollectionCreated")
    (cl-pushnew (car args) secrets-collection-paths))
   ((string-equal (dbus-event-member-name last-input-event) "CollectionDeleted")
    (setq secrets-collection-paths
	  (delete (car args) secrets-collection-paths)))))

(defun secrets-get-collections ()
  "Return the object paths of all available collections."
  (setq secrets-collection-paths
	(or secrets-collection-paths
	    (dbus-get-property
	     :session secrets-service secrets-path
	     secrets-interface-service "Collections"))))

(defun secrets-get-collection-properties (collection-path)
  "Return all properties of collection identified by COLLECTION-PATH."
  (unless (secrets-empty-path collection-path)
    (dbus-get-all-properties
     :session secrets-service collection-path
     secrets-interface-collection)))

(defun secrets-get-collection-property (collection-path property)
  "Return property PROPERTY of collection identified by COLLECTION-PATH."
  (unless (or (secrets-empty-path collection-path) (not (stringp property)))
    (dbus-get-property
     :session secrets-service collection-path
     secrets-interface-collection property)))

(defun secrets-list-collections ()
  "Return a list of collection names."
  (mapcar
   (lambda (collection-path)
     (if (string-equal collection-path secrets-session-collection-path)
	 "session"
       (secrets-get-collection-property collection-path "Label")))
   (secrets-get-collections)))

(defun secrets-collection-path (collection)
  "Return the object path of collection labeled COLLECTION.
If COLLECTION is nil, return the session collection path.
If there is no such COLLECTION, return nil."
  (or
   ;; The "session" collection.
   (if (or (null collection) (string-equal "session" collection))
       secrets-session-collection-path)
   ;; Check for an alias.
   (let ((collection-path
	  (dbus-call-method
	   :session secrets-service secrets-path
	   secrets-interface-service "ReadAlias" collection)))
     (unless (secrets-empty-path collection-path)
       collection-path))
   ;; Check the collections.
   (catch 'collection-found
     (dolist (collection-path (secrets-get-collections) nil)
       (when (string-equal
	      collection
	      (secrets-get-collection-property collection-path "Label"))
	 (throw 'collection-found collection-path))))))

(defun secrets-create-collection (collection &optional alias)
  "Create collection labeled COLLECTION if it doesn't exist.
Set ALIAS as alias of the collection.  Return the D-Bus object
path for collection."
  (let ((collection-path (secrets-collection-path collection)))
    ;; Create the collection.
    (when (secrets-empty-path collection-path)
      (setq collection-path
	    (secrets-prompt
	     (cadr
	      ;; "CreateCollection" returns the prompt path as second arg.
	      (dbus-call-method
	       :session secrets-service secrets-path
	       secrets-interface-service "CreateCollection"
	       `(:array
		 (:dict-entry ,(concat secrets-interface-collection ".Label")
			      (:variant ,collection)))
	       (or alias ""))))))
    ;; Return object path of the collection.
    collection-path))

(defun secrets-get-alias (alias)
  "Return the collection name ALIAS is referencing to.
For the time being, only the alias \"default\" is supported."
  (secrets-get-collection-property
   (dbus-call-method
    :session secrets-service secrets-path
    secrets-interface-service "ReadAlias" alias)
   "Label"))

(defun secrets-set-alias (collection alias)
  "Set ALIAS as alias of collection labeled COLLECTION.
For the time being, only the alias \"default\" is supported."
  (let ((collection-path (secrets-collection-path collection)))
    (unless (secrets-empty-path collection-path)
      (dbus-call-method
       :session secrets-service secrets-path
       secrets-interface-service "SetAlias"
       alias :object-path collection-path))))

(defun secrets-delete-alias (alias)
  "Delete ALIAS, referencing to a collection."
  (dbus-call-method
   :session secrets-service secrets-path
   secrets-interface-service "SetAlias"
   alias :object-path secrets-empty-path))

(defun secrets-lock-collection (collection)
  "Lock collection labeled COLLECTION.
If successful, return the object path of the collection."
  (let ((collection-path (secrets-collection-path collection)))
    (unless (secrets-empty-path collection-path)
      (secrets-prompt
       (cadr
	(dbus-call-method
	 :session secrets-service secrets-path secrets-interface-service
	 "Lock" `(:array :object-path ,collection-path)))))
    collection-path))

(defun secrets-unlock-collection (collection)
  "Unlock collection labeled COLLECTION.
If successful, return the object path of the collection."
  (let ((collection-path (secrets-collection-path collection)))
    (unless (secrets-empty-path collection-path)
      (secrets-prompt
       (cadr
	(dbus-call-method
	 :session secrets-service secrets-path secrets-interface-service
	 "Unlock" `(:array :object-path ,collection-path)))))
    collection-path))

(defun secrets-delete-collection (collection)
  "Delete collection labeled COLLECTION."
  (let ((collection-path (secrets-collection-path collection)))
    (unless (secrets-empty-path collection-path)
      (secrets-prompt
       (dbus-call-method
	:session secrets-service collection-path
	secrets-interface-collection "Delete")))))

;;; Items.

(defun secrets-get-items (collection-path)
  "Return the object paths of all available items in COLLECTION-PATH."
  (unless (secrets-empty-path collection-path)
    (dbus-get-property
     :session secrets-service collection-path
     secrets-interface-collection "Items")))

(defun secrets-get-item-properties (item-path)
  "Return all properties of item identified by ITEM-PATH."
  (unless (secrets-empty-path item-path)
    (dbus-get-all-properties
     :session secrets-service item-path
     secrets-interface-item)))

(defun secrets-get-item-property (item-path property)
  "Return property PROPERTY of item identified by ITEM-PATH."
  (unless (or (secrets-empty-path item-path) (not (stringp property)))
    (dbus-get-property
     :session secrets-service item-path
     secrets-interface-item property)))

(defun secrets-list-items (collection)
  "Return a list of all item labels of COLLECTION."
  (let ((collection-path (secrets-unlock-collection collection)))
    (unless (secrets-empty-path collection-path)
      (mapcar
       (lambda (item-path)
	 (secrets-get-item-property item-path "Label"))
       (secrets-get-items collection-path)))))

(defun secrets-search-item-paths (collection &rest attributes)
  "Search items in COLLECTION with ATTRIBUTES.
ATTRIBUTES are key-value pairs.  The keys are keyword symbols,
starting with a colon.  Example:

  (secrets-search-item-paths \"Tramp collection\" :user \"joe\")

The object paths of the found items are returned as list."
  (let ((collection-path (secrets-unlock-collection collection))
	props)
    (unless (secrets-empty-path collection-path)
      ;; Create attributes list.
      (while (consp (cdr attributes))
	(unless (keywordp (car attributes))
	  (error 'wrong-type-argument (car attributes)))
        (unless (stringp (cadr attributes))
          (error 'wrong-type-argument (cadr attributes)))
	(setq props (append
		     props
		     `((:dict-entry
			,(substring (symbol-name (car attributes)) 1)
			,(cadr attributes))))
	      attributes (cddr attributes)))
      ;; Search.  The result is a list of object paths.
      (dbus-call-method
       :session secrets-service collection-path
       secrets-interface-collection "SearchItems"
       (if props
	   (cons :array props)
	 '(:array :signature "{ss}"))))))

(defun secrets-search-items (collection &rest attributes)
  "Search items in COLLECTION with ATTRIBUTES.
ATTRIBUTES are key-value pairs.  The keys are keyword symbols,
starting with a colon.  Example:

  (secrets-search-items \"Tramp collection\" :user \"joe\")

The object labels of the found items are returned as list."
  (mapcar
   (lambda (item-path) (secrets-get-item-property item-path "Label"))
   (apply 'secrets-search-item-paths collection attributes)))

(defun secrets-create-item (collection item password &rest attributes)
  "Create a new item in COLLECTION with label ITEM and password PASSWORD.
The label ITEM does not have to be unique in COLLECTION.
ATTRIBUTES are key-value pairs set for the created item.  The
keys are keyword symbols, starting with a colon.  Example:

  (secrets-create-item \"Tramp collection\" \"item\" \"geheim\"
   :method \"sudo\" :user \"joe\" :host \"remote-host\")

The key `:xdg:schema' determines the scope of the item to be
generated, i.e. for which applications the item is intended for.
This is just a string like \"org.freedesktop.NetworkManager.Mobile\"
or \"org.gnome.OnlineAccounts\", the other required keys are
determined by this.  If no `:xdg:schema' is given,
\"org.freedesktop.Secret.Generic\" is used by default.

The object path of the created item is returned."
  (let ((collection-path (secrets-unlock-collection collection))
	result props)
    (unless (secrets-empty-path collection-path)
      ;; Set default type if needed.
      (unless (member :xdg:schema attributes)
        (setq attributes
              (append
               attributes `(:xdg:schema ,secrets-interface-item-type-generic))))
      ;; Create attributes list.
      (while (consp (cdr attributes))
	(unless (keywordp (car attributes))
	  (error 'wrong-type-argument (car attributes)))
        (unless (stringp (cadr attributes))
          (error 'wrong-type-argument (cadr attributes)))
	(setq props (append
		     props
		     `((:dict-entry
			,(substring (symbol-name (car attributes)) 1)
			,(cadr attributes))))
	      attributes (cddr attributes)))
      ;; Create the item.
      (setq result
	    (dbus-call-method
	     :session secrets-service collection-path
	     secrets-interface-collection "CreateItem"
	     ;; Properties.
	     (append
	      `(:array
		(:dict-entry ,(concat secrets-interface-item ".Label")
			     (:variant ,item)))
	      (when props
		`((:dict-entry ,(concat secrets-interface-item ".Attributes")
			       (:variant ,(append '(:array) props))))))
	     ;; Secret.
	     (append
	      `(:struct :object-path ,secrets-session-path
			(:array :signature "y") ;; No parameters.
			,(dbus-string-to-byte-array password))
	      ;; We add the content_type.  In backward compatibility
	      ;; mode, nil is appended, which means nothing.
	      secrets-struct-secret-content-type)
	     ;; Do not replace. Replace does not seem to work.
	     nil))
      (secrets-prompt (cadr result))
      ;; Return the object path.
      (car result))))

(defun secrets-item-path (collection item)
  "Return the object path of item labeled ITEM in COLLECTION.
If there are several items labeled ITEM, it is undefined which
one is returned.  If there is no such item, return nil.

ITEM can also be an object path, which is returned if contained in COLLECTION."
  (let ((collection-path (secrets-unlock-collection collection)))
    (or (and (member item (secrets-get-items collection-path)) item)
        (catch 'item-found
          (dolist (item-path (secrets-get-items collection-path))
	    (when (string-equal
                   item (secrets-get-item-property item-path "Label"))
	         (throw 'item-found item-path)))))))

(defun secrets-get-secret (collection item)
  "Return the secret of item labeled ITEM in COLLECTION.
If there are several items labeled ITEM, it is undefined which
one is returned.  If there is no such item, return nil.

ITEM can also be an object path, which is used if contained in COLLECTION."
  (let ((item-path (secrets-item-path collection item)))
    (unless (secrets-empty-path item-path)
      (dbus-byte-array-to-string
       (nth 2
	(dbus-call-method
	 :session secrets-service item-path secrets-interface-item
	 "GetSecret" :object-path secrets-session-path))))))

(defun secrets-get-attributes (collection item)
  "Return the lookup attributes of item labeled ITEM in COLLECTION.
If there are several items labeled ITEM, it is undefined which
one is returned.  If there is no such item, or the item has no
attributes, return nil.

ITEM can also be an object path, which is used if contained in COLLECTION."
  (let ((item-path (secrets-item-path collection item)))
    (unless (secrets-empty-path item-path)
      (mapcar
       (lambda (attribute)
	 (cons (intern (concat ":" (car attribute))) (cadr attribute)))
       (dbus-get-property
	:session secrets-service item-path
	secrets-interface-item "Attributes")))))

(defun secrets-get-attribute (collection item attribute)
  "Return the value of ATTRIBUTE of item labeled ITEM in COLLECTION.
If there are several items labeled ITEM, it is undefined which
one is returned.  If there is no such item, or the item doesn't
own this attribute, return nil.

ITEM can also be an object path, which is used if contained in COLLECTION."
  (cdr (assoc attribute (secrets-get-attributes collection item))))

(defun secrets-delete-item (collection item)
  "Delete item labeled ITEM in COLLECTION.
If there are several items labeled ITEM, it is undefined which
one is deleted.

ITEM can also be an object path, which is used if contained in COLLECTION."
  (let ((item-path (secrets-item-path collection item)))
    (unless (secrets-empty-path item-path)
      (secrets-prompt
       (dbus-call-method
	:session secrets-service item-path
	secrets-interface-item "Delete")))))

;;; Visualization.

(defvar secrets-mode-map
  (let ((map (make-sparse-keymap)))
    (set-keymap-parent map (make-composed-keymap special-mode-map widget-keymap))
    (define-key map "n" 'next-line)
    (define-key map "p" 'previous-line)
    (define-key map "z" 'kill-current-buffer)
    map)
  "Keymap used in `secrets-mode' buffers.")

(define-derived-mode secrets-mode special-mode "Secrets"
  "Major mode for presenting password entries retrieved by Security Service.
In this mode, widgets represent the search results.

\\{secrets-mode-map}"
  (setq buffer-undo-list t)
  (setq-local revert-buffer-function
              #'secrets-show-collections)
  ;; When we toggle, we must set temporary widgets.
  (add-hook 'tree-widget-after-toggle-functions
            #'secrets-tree-widget-after-toggle-function nil t))

;; It doesn't make sense to call it interactively.
(put 'secrets-mode 'disabled t)

;; We autoload `secrets-show-secrets' only on systems with D-Bus support.
;;;###autoload(when (featurep 'dbusbind)
;;;###autoload  (autoload 'secrets-show-secrets "secrets" nil t))

(defun secrets-show-secrets ()
  "Display a list of collections from the Secret Service API.
The collections are in tree view, that means they can be expanded
to the corresponding secret items, which could also be expanded
to their attributes."
  (interactive)

  ;; Check, whether the Secret Service API is enabled.
  (if (null secrets-enabled)
      (message "Secret Service not available")

    ;; Create the search buffer.
    (with-current-buffer (get-buffer-create "*Secrets*")
      (switch-to-buffer-other-window (current-buffer))
      ;; Initialize buffer with `secrets-mode'.
      (secrets-mode)
      (secrets-show-collections))))

(defun secrets-show-collections (&optional _ignore _noconfirm)
  "Show all available collections."
  (let ((inhibit-read-only t))
    (erase-buffer)
    (tree-widget-set-theme "folder")
    (dolist (coll (secrets-list-collections))
      (widget-create
     `(tree-widget
       :tag ,coll
       :collection ,coll
       :open nil
       :sample-face bold
       :expander secrets-expand-collection)))))

(defun secrets-expand-collection (widget)
  "Expand items of collection shown as WIDGET."
  (let ((coll (widget-get widget :collection)))
    (mapcar
     (lambda (item)
       `(tree-widget
	 :tag ,item
	 :collection ,coll
	 :item ,item
	 :open nil
	 :sample-face bold
	 :expander secrets-expand-item))
     (secrets-list-items coll))))

(defun secrets-expand-item (widget)
  "Expand password and attributes of item shown as WIDGET."
  (let* ((coll (widget-get widget :collection))
	 (item (widget-get widget :item))
	 (attributes (secrets-get-attributes coll item))
	 ;; padding is needed to format attribute names.
	 (padding
	  (apply
	   'max
	   (cons
	    (1+ (length "password"))
	    (mapcar
	     ;; Attribute names have a leading ":", which will be suppressed.
	     (lambda (attribute) (length (symbol-name (car attribute))))
	     attributes)))))
    (cons
     ;; The password widget.
     `(editable-field :tag "password"
		      :secret ?*
		      :value ,(secrets-get-secret coll item)
		      :sample-face widget-button-pressed
		      ;; We specify :size in order to limit the field.
		      :size 0
		      :format ,(concat
				"%{%t%}:"
				(make-string (- padding (length "password")) ? )
				"%v\n"))
     (mapcar
      (lambda (attribute)
	(let ((name (substring (symbol-name (car attribute)) 1))
	      (value (cdr attribute)))
	  ;; The attribute widget.
	  `(editable-field :tag ,name
			   :value ,value
			   :sample-face widget-documentation
			   ;; We specify :size in order to limit the field.
			   :size 0
			   :format ,(concat
				     "%{%t%}:"
				     (make-string (- padding (length name)) ? )
				     "%v\n"))))
      attributes))))

(defun secrets-tree-widget-after-toggle-function (widget &rest _ignore)
  "Add a temporary widget to show the password."
  (dolist (child (widget-get widget :children))
    (when (widget-member child :secret)
      (goto-char (widget-field-end child))
      (widget-insert " ")
      (widget-create-child-and-convert
       child 'push-button
       :notify 'secrets-tree-widget-show-password
       "Show password")))
  (widget-setup))

(defun secrets-tree-widget-show-password (widget &rest _ignore)
  "Show password, and remove temporary widget."
  (let ((parent (widget-get widget :parent)))
    (widget-put parent :secret nil)
    (widget-default-value-set parent (widget-get parent :value))
    (widget-setup)))

;;; Initialization.

(when (dbus-ping :session secrets-service 100)

  (secrets-open-session)

  ;; We must reset all variables, when there is a new instance of the
  ;; "org.freedesktop.secrets" service.
  (dbus-register-signal
   :session dbus-service-dbus dbus-path-dbus
   dbus-interface-dbus "NameOwnerChanged"
   (lambda (&rest args)
     (when secrets-debug (message "Secret Service has changed: %S" args))
     (setq secrets-session-path secrets-empty-path
	   secrets-prompt-signal nil
	   secrets-collection-paths nil))
   secrets-service)

  ;; We want to refresh our cache, when there is a change in
  ;; collections.
  (dbus-register-signal
   :session secrets-service secrets-path
   secrets-interface-service "CollectionCreated"
   'secrets-collection-handler)

  (dbus-register-signal
   :session secrets-service secrets-path
   secrets-interface-service "CollectionDeleted"
   'secrets-collection-handler)

  ;; We shall inform, whether the secret service is enabled on this
  ;; machine.
  (setq secrets-enabled t))

(provide 'secrets)

;;; TODO:

;; * secrets-debug should be structured like auth-source-debug to
;;   prevent leaking sensitive information.  Right now I don't see
;;   anything sensitive though.
;; * Check, whether the dh-ietf1024-aes128-cbc-pkcs7 algorithm can be
;;   used for the transfer of the secrets.  Currently, we use the
;;   plain algorithm.