2 # -*- mode: python; coding: utf-8-unix; -*-
5 import argparse, errno, os, re, readline, subprocess, sys, tempfile, textwrap
8 if sys.version_info[0] == 2:
10 from tkMessageBox import *
12 from ScrolledText import *
14 def input_string (prompt=""):
15 return raw_input (prompt)
16 elif sys.version_info[0] > 2:
18 from tkinter.messagebox import *
19 from tkinter.tix import *
20 from tkinter.scrolledtext import *
22 def input_string (prompt=""):
25 raise Exception ("Unsupported Python version {}".format (sys.version_info))
29 return textwrap.dedent (text).strip ()
33 return textwrap.dedent (text).lstrip ()
37 return textwrap.fill (dedented (text), width = 72)
41 return textwrap.fill (ldedented (text), width = 72)
44 def read_input_string (prompt = "", default = ""):
46 readline.set_startup_hook (lambda: readline.insert_text (default))
49 return input_string(prompt)
51 readline.set_startup_hook()
54 def parse_arguments ():
55 parser = argparse.ArgumentParser ()
60 version = "crypto-install.py version GIT-TAG (GIT-COMMIT/GIT-BRANCH)",
61 help = "Display version.")
65 action = "store_false",
66 help = "Disable GUI, use text mode.")
67 gnupg_group = parser.add_argument_group (
69 "Options related to the GnuPG setup.")
70 gnupg_group.add_argument (
73 action = "store_false",
74 help = "Disable GnuPG setup.")
75 gnupg_group.add_argument (
78 default = os.getenv("GNUPGHOME") or "~/.gnupg",
80 help = "Default directory for GnuPG files.")
81 openssh_group = parser.add_argument_group (
83 "Options related to the OpenSSH setup.")
84 openssh_group.add_argument (
87 action = "store_false",
88 help = "Disable OpenSSH setup.")
89 openssh_group.add_argument (
91 dest = "openssh_home",
94 help = "Default directory for OpenSSH files.")
95 return parser.parse_args ()
98 def ensure_directories (path, mode = 0o777):
100 os.makedirs (path, mode)
101 except OSError as exception:
102 if exception.errno != errno.EEXIST:
107 return os.getenv ("FULLNAME")
110 def default_email ():
111 return os.getenv ("EMAIL")
114 def default_comment ():
118 def default_hostname ():
119 return subprocess.check_output ("hostname").strip ()
122 def default_username ():
123 return os.getenv ("USER")
126 def valid_email (value):
127 return re.match (".+@.+", value)
130 def valid_name (value):
131 return value.strip () != ""
134 def valid_comment (value):
138 def gnupg_exists (arguments):
139 gnupg_home = os.path.expanduser (arguments.gnupg_home)
140 gnupg_secring = os.path.join (gnupg_home, "secring.gpg")
142 return os.path.exists (gnupg_secring)
145 def openssh_exists (arguments):
146 openssh_home = os.path.expanduser (arguments.openssh_home)
147 openssh_config = os.path.join (openssh_home, "config")
148 openssh_key = os.path.join (openssh_home, "id_rsa")
150 return os.path.exists (openssh_config) and os.path.exists (openssh_key)
153 # TODO: verify phrase at least once
154 # TODO: use better labels
155 def input_passphrase (arguments):
156 batch_passphrase = ldedented ("""
160 """).format (subprocess.check_output ("tty").strip (),
163 batch_env = dict (os.environ)
165 batch_passphrase += ldedented ("""
168 """).format (os.getenv ("XAUTHORITY"),
169 os.getenv ("DISPLAY"))
171 del batch_env["DISPLAY"]
173 batch_passphrase += \
174 "GET_PASSPHRASE --data --check --qualitybar X X Passphrase X\n"
176 passphrase_process = subprocess.Popen (["gpg-agent", "--server"],
177 stdin = subprocess.PIPE,
178 stdout = subprocess.PIPE,
179 stderr = subprocess.PIPE,
181 (stdout, stderr) = passphrase_process.communicate (batch_passphrase.encode ("UTF-8"))
183 if passphrase_process.returncode != 0:
184 raise Exception ("Couldn't read passphrase.")
186 for line in stdout.splitlines ():
187 if line.decode ("UTF-8").startswith ("D "):
193 def gnupg_setup (arguments, name = None, email = None, comment = None):
194 gnupg_home = os.path.expanduser (arguments.gnupg_home)
195 gnupg_secring = os.path.join (gnupg_home, "secring.gpg")
197 if gnupg_exists (arguments):
198 print ("GnuPG secret keyring already exists at '{}'."
199 .format (gnupg_secring))
202 if not arguments.gui:
204 No default GnuPG key available. Please enter your information to
205 create a new key."""))
207 name = read_input_string ("What is your name (e.g. 'John Doe')? ",
210 email = read_input_string (dedented ("""
211 What is your email address (e.g. 'test@example.com')? """),
214 comment = read_input_string (dedented ("""
215 What is your comment phrase, if any (e.g. 'key for 2014')? """),
218 if not os.path.exists (gnupg_home):
219 print ("Creating GnuPG directory at '{}'.".format (gnupg_home))
220 ensure_directories (gnupg_home, 0o700)
222 with tempfile.NamedTemporaryFile () as tmp:
223 batch_key = ldedented ("""
232 """).format (name, email)
235 batch_key += "Name-Comment: {}\n".format (comment)
237 tmp.write (batch_key.encode ("UTF-8"))
240 batch_env = dict (os.environ)
241 if not arguments.gui:
242 del batch_env["DISPLAY"]
244 gnupg_process = subprocess.Popen (["gpg2",
245 "--homedir", gnupg_home,
246 "--batch", "--gen-key", tmp.name],
247 stdin = subprocess.PIPE,
248 stdout = subprocess.PIPE,
249 stderr = subprocess.STDOUT,
252 # TODO: argh. there has to be a better way
253 gnupg_process.stdin.close ()
254 while gnupg_process.poll () is None:
255 sys.stdout.write (gnupg_process.stdout.readline ())
258 line = gnupg_process.stdout.readline ()
261 sys.stdout.write (line.decode ("UTF-8"))
263 if gnupg_process.returncode != 0:
264 raise Exception ("Couldn't create GnuPG key.")
267 def openssh_setup (arguments, comment = None):
268 openssh_home = os.path.expanduser (arguments.openssh_home)
269 openssh_config = os.path.join (openssh_home, "config")
271 if not os.path.exists (openssh_config):
272 print ("Creating OpenSSH directory at '{}'.".format (openssh_home))
273 ensure_directories (openssh_home, 0o700)
275 print ("Creating OpenSSH configuration at '{}'."
276 .format (openssh_config))
277 with open (openssh_config, "w") as config:
278 config.write (ldedented ("""
283 openssh_key = os.path.join (openssh_home, "id_rsa")
285 if os.path.exists (openssh_key):
286 print ("OpenSSH key already exists at '{}'.".format (openssh_key))
289 openssh_key_dsa = os.path.join (openssh_home, "id_dsa")
291 if os.path.exists (openssh_key_dsa):
292 print ("OpenSSH key already exists at '{}'.".format (openssh_key_dsa))
295 print (filled ("No OpenSSH key available. Generating new key."))
297 if not arguments.gui:
298 comment = "{}@{}".format (default_username (), default_hostname ())
299 comment = read_input_string (ldedented ("""
300 What is your comment phrase (e.g. 'user@mycomputer')? """), comment)
302 passphrase = input_passphrase (arguments)
304 batch_env = dict (os.environ)
305 if not arguments.gui:
306 del batch_env["DISPLAY"]
308 # TODO: is it somehow possible to pass the password on stdin?
309 openssh_process = subprocess.Popen (["ssh-keygen",
313 stdin = subprocess.PIPE,
314 stdout = subprocess.PIPE,
315 stderr = subprocess.STDOUT,
318 # TODO: argh. there has to be a better way
319 openssh_process.stdin.close ()
320 while openssh_process.poll () is None:
321 sys.stdout.write (openssh_process.stdout.readline ())
324 line = openssh_process.stdout.readline ()
327 sys.stdout.write (line.decode ("UTF-8"))
329 if openssh_process.returncode != 0:
330 raise Exception ("Couldn't create OpenSSH key.")
333 # http://www.blog.pythonlibrary.org/2014/07/14/tkinter-redirecting-stdout-stderr/
334 class RedirectText (object):
335 def __init__ (self, widget):
338 def write (self, string):
339 self.widget.insert (END, string)
342 class CryptoInstallProgress (Toplevel):
344 Toplevel.__init__ (self)
346 self.create_widgets ()
348 def create_widgets (self):
349 self.balloon = Balloon (self, initwait = 250)
351 self.text = ScrolledText (self)
352 self.text.pack (fill = BOTH, expand = True)
354 self.redirect = RedirectText (self.text)
356 self._quit = Button (self)
357 self._quit["text"] = "Quit"
358 self._quit["command"] = self.quit
359 self.balloon.bind_widget (self._quit,
360 msg = "Quit the program immediately")
364 class CryptoInstall (Tk):
365 def __init__ (self, arguments):
368 self.arguments = arguments
370 self.resizable (width = False, height = False)
371 self.title ("Crypto Install Wizard")
373 self.create_widgets ()
375 def create_widgets (self):
376 self.balloon = Balloon (self, initwait = 250)
378 self.info_frame = Frame (self)
379 self.info_frame.pack (fill = X)
381 self.user_label = Label (self.info_frame)
382 self.user_label["text"] = "Username"
383 self.user_label.grid ()
385 self.user_var = StringVar ()
386 self.user_var.set (default_username ())
387 self.user_var.trace ("w", self.update_widgets)
389 self.user = Entry (self.info_frame, textvariable = self.user_var,
391 self.balloon.bind_widget (self.user, msg = dedented ("""
392 Username on the local machine (e.g. 'user')
394 self.user.grid (row = 0, column = 1)
396 self.host_label = Label (self.info_frame)
397 self.host_label["text"] = "Host Name"
398 self.host_label.grid ()
400 self.host_var = StringVar ()
401 self.host_var.set (default_hostname ())
402 self.host_var.trace ("w", self.update_widgets)
404 self.host = Entry (self.info_frame, textvariable = self.host_var,
406 self.balloon.bind_widget (self.host, msg = dedented ("""
407 Host name of the local machine (e.g. 'mycomputer')
409 self.host.grid (row = 1, column = 1)
411 self.name_label = Label (self.info_frame)
412 self.name_label["text"] = "Full Name"
413 self.name_label.grid ()
415 self.name_var = StringVar ()
416 self.name_var.set (default_name ())
417 self.name_var.trace ("w", self.update_widgets)
419 self.name = Entry (self.info_frame, textvariable = self.name_var)
420 self.balloon.bind_widget (self.name, msg = dedented ("""
421 Full name as it should appear in the key description (e.g. 'John Doe')
423 self.name.grid (row = 2, column = 1)
425 self.email_label = Label (self.info_frame)
426 self.email_label["text"] = "Email address"
427 self.email_label.grid ()
429 self.email_var = StringVar ()
430 self.email_var.set (default_email ())
431 self.email_var.trace ("w", self.update_widgets)
433 self.email = Entry (self.info_frame, textvariable = self.email_var)
434 self.balloon.bind_widget (self.email, msg = dedented ("""
435 Email address associated with the name (e.g. '<test@example.com>')
437 self.email.grid (row = 3, column = 1)
439 self.comment_label = Label (self.info_frame)
440 self.comment_label["text"] = "Comment phrase"
441 self.comment_label.grid ()
443 self.comment_var = StringVar ()
444 self.comment_var.set (default_comment ())
445 self.comment_var.trace ("w", self.update_widgets)
447 self.comment = Entry (self.info_frame, textvariable = self.comment_var)
448 self.balloon.bind_widget (self.comment, msg = dedented ("""
449 Comment phrase for the GnuPG key, if any (e.g. 'key for 2014')
451 self.comment.grid (row = 4, column = 1)
453 self.options_frame = Frame (self)
454 self.options_frame.pack (fill = X)
456 self.gnupg_label = Label (self.options_frame)
457 self.gnupg_label["text"] = "Generate GnuPG key"
458 self.gnupg_label.grid ()
460 self.gnupg_var = IntVar ()
461 self.gnupg_var.set (1 if self.arguments.gnupg else 0)
462 self.gnupg_var.trace ("w", self.update_widgets)
464 self.gnupg = Checkbutton (self.options_frame,
465 variable = self.gnupg_var)
466 self.gnupg.grid (row = 0, column = 1)
468 self.openssh_label = Label (self.options_frame)
469 self.openssh_label["text"] = "Generate OpenSSH key"
470 self.openssh_label.grid ()
472 self.openssh_var = IntVar ()
473 self.openssh_var.set (1 if self.arguments.openssh else 0)
474 self.openssh_var.trace ("w", self.update_widgets)
476 self.openssh = Checkbutton (self.options_frame,
477 variable = self.openssh_var)
478 self.openssh.grid (row = 1, column = 1)
480 self.button_frame = Frame (self)
481 self.button_frame.pack (fill = X)
483 self._generate = Button (self.button_frame)
484 self._generate["text"] = "Generate Keys"
485 self._generate["command"] = self.generate
486 self.balloon.bind_widget (
488 msg = "Generate the keys as configured above")
489 self._generate.pack (side = LEFT, fill = Y)
491 self._quit = Button (self.button_frame)
492 self._quit["text"] = "Quit"
493 self._quit["command"] = self.quit
494 self.balloon.bind_widget (self._quit,
495 msg = "Quit the program immediately")
496 self._quit.pack (side = LEFT)
498 self.update_widgets ()
500 def valid_state (self):
501 if not self.openssh_var.get () and not self.gnupg_var.get ():
504 if gnupg_exists (self.arguments) and openssh_exists (self.arguments):
507 if not valid_email (self.email_var.get ()):
510 if not valid_name (self.name_var.get ()):
513 if not valid_comment (self.comment_var.get ()):
518 def update_widgets (self, *args):
519 valid = self.valid_state ()
521 self._generate["state"] = NORMAL if valid else DISABLED
523 name = self.name_var.get ()
525 valid = valid_name (name)
526 self.name["fg"] = "black" if valid else "red"
527 self.name_label["fg"] = "black" if valid else "red"
529 email = self.email_var.get ()
531 valid = valid_email (email)
532 self.email["fg"] = "black" if valid else "red"
533 self.email_label["fg"] = "black" if valid else "red"
535 comment = self.comment_var.get ()
537 valid = valid_comment (comment)
538 self.comment["fg"] = "black" if valid else "red"
539 self.comment_label["fg"] = "black" if valid else "red"
541 exists = gnupg_exists (self.arguments)
542 self.gnupg["state"] = NORMAL if not exists else DISABLED
544 exists = openssh_exists (self.arguments)
545 self.openssh["state"] = NORMAL if not exists else DISABLED
548 if comment.strip () != "":
549 gnupg_key + " ({}) ".format (comment)
550 gnupg_key += "<{}>".format (email)
552 user = self.user_var.get ()
553 host = self.host_var.get ()
555 openssh_key = "{}@{}".format (user, host)
558 Generate a GnuPG key for '{}' and configure a default setup for it
559 """).format (gnupg_key)
561 self.balloon.bind_widget (self.gnupg, msg = msg)
562 self.balloon.bind_widget (self.gnupg_label, msg = msg)
565 Generate an OpenSSH key for '{}' and configure a default setup for it
566 """).format (openssh_key)
568 self.balloon.bind_widget (self.openssh, msg = msg)
569 self.balloon.bind_widget (self.openssh_label, msg = msg)
572 progress = CryptoInstallProgress ()
576 sys.stdout = progress.redirect
578 # TODO: capture and show stdout and stderr
579 if self.gnupg_var.get ():
580 gnupg_setup (self.arguments,
581 self.name_var.get (),
582 self.email_var.get (),
583 self.comment_var.get ())
584 self.update_widgets ()
586 if self.openssh_var.get ():
587 comment = "{}@{}".format (self.user_var.get (),
588 self.host_var.get ())
589 openssh_setup (self.arguments, comment)
590 self.update_widgets ()
596 arguments = parse_arguments ()
599 # TODO: use gtk instead? would be more consistent with the pinentry style
600 # (assuming it's using gtk)
601 CryptoInstall (arguments).mainloop ()
604 gnupg_setup (arguments)
606 if arguments.openssh:
607 openssh_setup (arguments)
610 if __name__ == "__main__":