· 6 years ago · Sep 05, 2019, 07:38 AM
1#!/usr/bin/env python3
2
3import argparse
4from getpass import getpass
5import gi
6gi.require_version("Secret", "1")
7from gi.repository import Secret
8from pykeepass import PyKeePass
9
10
11def get_keyrings(secservice):
12 keyrings = []
13 for collection in secservice.get_collections():
14 if not collection.get_label() == "":
15 keyrings.append(collection.get_label())
16 return keyrings
17
18
19def unlock_prompt(secservice, group_names):
20 collections = collections_from_names(secservice, group_names)
21 unlocked, unlockedcollections = secservice.unlock_sync(collections, None)
22 return unlockedcollections
23
24# abstaining from using map() + lambda here for readability
25def collections_from_names(secservice, keyringnames):
26 collections = []
27 for collection in secservice.get_collections():
28 if collection.get_label() in set(keyringnames):
29 collections.append(collection)
30 return collections
31
32
33if __name__ == "__main__":
34 secret = Secret.Service.get_sync(Secret.ServiceFlags.LOAD_COLLECTIONS, None)
35
36 parser = argparse.ArgumentParser(description="Export your secrets from a Secret Service API-"
37 "compatible keyring to an existing KeePassXC database. "
38 "Both version 3 and 4 of .kdbx files are supported. "
39 "CREATE BACKUPS AND PROCEED CAREFULLY TO PREVENT DATA LOSS OR LEAKAGE.")
40 parser.add_argument("dbfile", metavar="dbfile.kdbx", type=str, default="",
41 help="The KeePassXC database file you want to export to. NOTE: This file must already exist. "
42 "Also make sure to specify dbfile before any options that might use more than one "
43 "argument such as \"-m\" or \"-s\".")
44 parser.add_argument("-m", "--merge-keyrings", dest="merge_keyrings", action="store_true",
45 help="All keyrings will exported to a single group in your db-file. "
46 "NOTE: Usage of \"-p\" in conjunction is advised or "
47 "all your exported entries will appear in your Root Group.")
48 parser.add_argument("-s", "--select-keyrings", nargs="+", metavar="keyring", dest="selected_keyrings",
49 action="store",
50 help="Select the keyrings that should be exported. "
51 "Valid choices are: " + ", ".join(get_keyrings(secret)))
52 parser.add_argument("-f", "--force", dest="overwrite_duplicates",
53 action="store_true",
54 help="If the groups or entries you're trying to export already exist, "
55 "this option will force overwriting duplicate entries. Without this flag, "
56 "duplicates will be saved alongside already-existing entries to prevent data loss. "
57 "NOTE: If a group already exists, it will be used "
58 "regardless of this flag. To mitigate this, use \"-p\".")
59 parser.add_argument("-V", "--verbose", dest="verbose",
60 action="store_true",
61 help="Print the labels of all duplicate entries that will be saved or overwritten if \"-f\" "
62 "is used.")
63 parser.add_argument("-p", "--parent-group", nargs="?", metavar="parent-name", dest="parent_group",
64 action="store", const="Keyring Export", default=None,
65 help="Choose a parent-name that should be used as a parent-group for exporting your keyrings. "
66 "If no parent-name is given, \"Keyring Export\" will be used. "
67 "If the chosen group does not exist, it will be created.")
68 parser.add_argument("-k", "--keyfile", metavar="keyfile", type=str, nargs=1,
69 help="Use this flag to specify the location of your keyfile/yubikey if you use one.")
70
71 args = parser.parse_args()
72
73 if not args.selected_keyrings:
74 keyrings = get_keyrings(secret)
75 else:
76 keyrings = args.selected_keyrings
77
78 try:
79 if args.keyfile:
80 keydb = PyKeePass(args.dbfile, getpass(prompt="Password for " + args.dbfile + ": "), args.keyfile[0])
81 else:
82 keydb = PyKeePass(args.dbfile, getpass(prompt="Password for " + args.dbfile + ": "))
83 except BaseException as e:
84 print(str(e))
85 else:
86 if args.parent_group:
87 parentgroup = keydb.find_groups(name=args.parent_group, first=True)
88 if parentgroup is None:
89 parentgroup = keydb.add_group(keydb.root_group, args.parent_group)
90 else:
91 parentgroup = keydb.root_group
92
93 # unlockedkeyrings is necessary to relock keyrings after export
94 unlockedkeyrings = unlock_prompt(secret, keyrings)
95 # keyrings that were already unlocked don't appear in unlockedkeyrings, we miss keyrings if we'd use
96 # for unlockedkeyring in unlockedkeyrings
97 for unlockedkeyring in collections_from_names(secret, keyrings):
98 if args.merge_keyrings:
99 exportgroup = parentgroup
100 else:
101 exportgroup = keydb.find_groups(name=unlockedkeyring.get_label(), group=parentgroup, first=True)
102 if exportgroup is None:
103 exportgroup = keydb.add_group(parentgroup, unlockedkeyring.get_label())
104
105 duplicates = 0
106
107 for item in unlockedkeyring.get_items():
108 item.load_secret_sync(None)
109 entry = keydb.find_entries(title=item.get_label(), group=exportgroup, first=True)
110
111 if args.overwrite_duplicates:
112 if entry is not None:
113 if args.verbose:
114 print("[" + exportgroup.name + "] Overwriting: " + item.get_label())
115 entry.title = item.get_label()
116 entry.username = ""
117 entry.password = item.get_secret().get_text()
118 else:
119 entry = keydb.add_entry(exportgroup, title=item.get_label(), username="",
120 password=item.get_secret().get_text()
121 , force_creation=True)
122 # for key, value in ... doesn't seam to work with GHashTable
123 for key in item.get_attributes():
124 entry.set_custom_property(key, str(item.get_attributes()[key]))
125 else:
126 newentry = keydb.add_entry(exportgroup, title=item.get_label(), username="",
127 password=item.get_secret().get_text()
128 , force_creation=True)
129 for key in item.get_attributes():
130 newentry.set_custom_property(key, str(item.get_attributes()[key]))
131 if entry is not None:
132 duplicates += 1
133 if args.verbose:
134 print("[" + exportgroup.name + "] Duplicate: " + item.get_label())
135
136 if duplicates > 0:
137 if duplicates == 1:
138 entries = " duplicate."
139 else:
140 entries = " duplicates."
141 print("Found " + str(duplicates) + " possible" + entries)
142 print("Use \"-f\" to force overwriting existing entries (based on their title).")
143
144 keydb.save()
145 # locks only keyrings that were unlocked during export.
146 secret.lock_sync(unlockedkeyrings, None)