2 # -*- mode: python; coding: utf-8-unix; -*-
5 import argparse, errno, os, re, readline, subprocess, sys, tempfile, textwrap, threading
8 if sys.version_info[0] == 2:
10 from tkMessageBox import *
12 from ScrolledText import *
15 def input_string (prompt=""):
16 return raw_input (prompt)
17 elif sys.version_info[0] > 2:
19 from tkinter.messagebox import *
20 from tkinter.tix import *
21 from tkinter.scrolledtext import *
24 def input_string (prompt=""):
27 raise Exception ("Unsupported Python version {}".format (sys.version_info))
31 return textwrap.dedent (text).strip ()
35 return textwrap.dedent (text).lstrip ()
39 return textwrap.fill (dedented (text), width = 72)
43 return textwrap.fill (ldedented (text), width = 72)
46 def read_input_string (prompt = "", default = ""):
48 readline.set_startup_hook (lambda: readline.insert_text (default))
51 return input_string(prompt)
53 readline.set_startup_hook()
56 def parse_arguments ():
57 parser = argparse.ArgumentParser ()
62 version = "crypto-install.py version GIT-TAG (GIT-COMMIT/GIT-BRANCH)",
63 help = "Display version.")
67 action = "store_false",
68 help = "Disable GUI, use text mode.")
69 gnupg_group = parser.add_argument_group (
71 "Options related to the GnuPG setup.")
72 gnupg_group.add_argument (
75 action = "store_false",
76 help = "Disable GnuPG setup.")
77 gnupg_group.add_argument (
80 default = os.getenv("GNUPGHOME") or "~/.gnupg",
82 help = "Default directory for GnuPG files.")
83 openssh_group = parser.add_argument_group (
85 "Options related to the OpenSSH setup.")
86 openssh_group.add_argument (
89 action = "store_false",
90 help = "Disable OpenSSH setup.")
91 openssh_group.add_argument (
93 dest = "openssh_home",
96 help = "Default directory for OpenSSH files.")
97 return parser.parse_args ()
100 def ensure_directories (path, mode = 0o777):
102 os.makedirs (path, mode)
103 except OSError as exception:
104 if exception.errno != errno.EEXIST:
109 return os.getenv ("FULLNAME")
112 def default_email ():
113 return os.getenv ("EMAIL")
116 def default_comment ():
120 def default_hostname ():
121 return subprocess.check_output ("hostname").strip ()
124 def default_username ():
125 return os.getenv ("USER")
128 def valid_email (value):
129 return re.match (".+@.+", value)
132 def valid_name (value):
133 return value.strip () != ""
136 def valid_comment (value):
140 def gnupg_exists (arguments):
141 gnupg_home = os.path.expanduser (arguments.gnupg_home)
142 gnupg_secring = os.path.join (gnupg_home, "secring.gpg")
144 return os.path.exists (gnupg_secring)
147 def openssh_exists (arguments):
148 openssh_home = os.path.expanduser (arguments.openssh_home)
149 openssh_config = os.path.join (openssh_home, "config")
150 openssh_key = os.path.join (openssh_home, "id_rsa")
152 return os.path.exists (openssh_config) and os.path.exists (openssh_key)
155 # TODO: verify phrase at least once
156 # TODO: use better labels
157 def input_passphrase (arguments):
158 batch_passphrase = ldedented ("""
162 """).format (subprocess.check_output ("tty").strip (),
165 batch_env = dict (os.environ)
167 batch_passphrase += ldedented ("""
170 """).format (os.getenv ("XAUTHORITY"),
171 os.getenv ("DISPLAY"))
173 del batch_env["DISPLAY"]
175 batch_passphrase += \
176 "GET_PASSPHRASE --data --check --qualitybar X X Passphrase X\n"
178 passphrase_process = subprocess.Popen (["gpg-agent", "--server"],
179 stdin = subprocess.PIPE,
180 stdout = subprocess.PIPE,
181 stderr = subprocess.PIPE,
183 (stdout, stderr) = passphrase_process.communicate (batch_passphrase.encode ("UTF-8"))
185 if passphrase_process.returncode != 0:
186 raise Exception ("Couldn't read passphrase.")
188 for line in stdout.splitlines ():
189 if line.decode ("UTF-8").startswith ("D "):
195 def gnupg_setup (arguments, name = None, email = None, comment = None):
196 gnupg_home = os.path.expanduser (arguments.gnupg_home)
197 gnupg_secring = os.path.join (gnupg_home, "secring.gpg")
199 if gnupg_exists (arguments):
200 print ("GnuPG secret keyring already exists at '{}'."
201 .format (gnupg_secring))
204 if not arguments.gui:
206 No default GnuPG key available. Please enter your information to
207 create a new key."""))
209 name = read_input_string ("What is your name (e.g. 'John Doe')? ",
212 email = read_input_string (dedented ("""
213 What is your email address (e.g. 'test@example.com')? """),
216 comment = read_input_string (dedented ("""
217 What is your comment phrase, if any (e.g. 'key for 2014')? """),
220 if not os.path.exists (gnupg_home):
221 print ("Creating GnuPG directory at '{}'.".format (gnupg_home))
222 ensure_directories (gnupg_home, 0o700)
224 with tempfile.NamedTemporaryFile () as tmp:
225 batch_key = ldedented ("""
234 """).format (name, email)
237 batch_key += "Name-Comment: {}\n".format (comment)
239 tmp.write (batch_key.encode ("UTF-8"))
242 batch_env = dict (os.environ)
243 if not arguments.gui:
244 del batch_env["DISPLAY"]
246 gnupg_process = subprocess.Popen (["gpg2",
247 "--homedir", gnupg_home,
248 "--batch", "--gen-key", tmp.name],
249 stdin = subprocess.PIPE,
250 stdout = subprocess.PIPE,
251 stderr = subprocess.STDOUT,
254 # TODO: argh. there has to be a better way
255 gnupg_process.stdin.close ()
256 while gnupg_process.poll () is None:
257 sys.stdout.write (gnupg_process.stdout.readline ())
260 line = gnupg_process.stdout.readline ()
263 sys.stdout.write (line.decode ("UTF-8"))
265 if gnupg_process.returncode != 0:
266 raise Exception ("Couldn't create GnuPG key.")
269 def openssh_setup (arguments, comment = None):
270 openssh_home = os.path.expanduser (arguments.openssh_home)
271 openssh_config = os.path.join (openssh_home, "config")
273 if not os.path.exists (openssh_config):
274 print ("Creating OpenSSH directory at '{}'.".format (openssh_home))
275 ensure_directories (openssh_home, 0o700)
277 print ("Creating OpenSSH configuration at '{}'."
278 .format (openssh_config))
279 with open (openssh_config, "w") as config:
280 config.write (ldedented ("""
285 openssh_key = os.path.join (openssh_home, "id_rsa")
287 if os.path.exists (openssh_key):
288 print ("OpenSSH key already exists at '{}'.".format (openssh_key))
291 openssh_key_dsa = os.path.join (openssh_home, "id_dsa")
293 if os.path.exists (openssh_key_dsa):
294 print ("OpenSSH key already exists at '{}'.".format (openssh_key_dsa))
297 print (filled ("No OpenSSH key available. Generating new key."))
299 if not arguments.gui:
300 comment = "{}@{}".format (default_username (), default_hostname ())
301 comment = read_input_string (ldedented ("""
302 What is your comment phrase (e.g. 'user@mycomputer')? """), comment)
304 passphrase = input_passphrase (arguments)
306 batch_env = dict (os.environ)
307 if not arguments.gui:
308 del batch_env["DISPLAY"]
310 # TODO: is it somehow possible to pass the password on stdin?
311 openssh_process = subprocess.Popen (["ssh-keygen",
315 stdin = subprocess.PIPE,
316 stdout = subprocess.PIPE,
317 stderr = subprocess.STDOUT,
320 # TODO: argh. there has to be a better way
321 openssh_process.stdin.close ()
322 while openssh_process.poll () is None:
323 sys.stdout.write (openssh_process.stdout.readline ())
326 line = openssh_process.stdout.readline ()
329 sys.stdout.write (line.decode ("UTF-8"))
331 if openssh_process.returncode != 0:
332 raise Exception ("Couldn't create OpenSSH key.")
335 # http://www.blog.pythonlibrary.org/2014/07/14/tkinter-redirecting-stdout-stderr/
336 # http://www.virtualroadside.com/blog/index.php/2012/11/10/glib-idle_add-for-tkinter-in-python/
337 class RedirectText (object):
338 def __init__ (self, root, widget):
342 self.queue = Queue ()
344 def write (self, string):
345 self.widget.insert (END, string)
347 def enqueue (self, value):
348 self.queue.put (value)
349 self.root.event_generate ("<<Idle>>", when = "tail")
352 class CryptoInstallProgress (Toplevel):
353 def __init__ (self, parent):
354 Toplevel.__init__ (self, parent)
358 self.create_widgets ()
360 def create_widgets (self):
361 self.balloon = Balloon (self, initwait = 250)
363 self.text = ScrolledText (self)
364 self.text.pack (fill = BOTH, expand = True)
366 self.redirect = RedirectText (self.parent, self.text)
368 self._quit = Button (self)
369 self._quit["text"] = "Quit"
370 self._quit["command"] = self.quit
371 self.balloon.bind_widget (self._quit,
372 msg = "Quit the program immediately")
376 class CryptoInstall (Tk):
377 def __init__ (self, arguments):
380 self.arguments = arguments
382 self.resizable (width = False, height = False)
383 self.title ("Crypto Install Wizard")
385 self.create_widgets ()
387 def create_widgets (self):
388 self.balloon = Balloon (self, initwait = 250)
390 self.info_frame = Frame (self)
391 self.info_frame.pack (fill = X)
393 self.user_label = Label (self.info_frame)
394 self.user_label["text"] = "Username"
395 self.user_label.grid ()
397 self.user_var = StringVar ()
398 self.user_var.set (default_username ())
399 self.user_var.trace ("w", self.update_widgets)
401 self.user = Entry (self.info_frame, textvariable = self.user_var,
403 self.balloon.bind_widget (self.user, msg = dedented ("""
404 Username on the local machine (e.g. 'user')
406 self.user.grid (row = 0, column = 1)
408 self.host_label = Label (self.info_frame)
409 self.host_label["text"] = "Host Name"
410 self.host_label.grid ()
412 self.host_var = StringVar ()
413 self.host_var.set (default_hostname ())
414 self.host_var.trace ("w", self.update_widgets)
416 self.host = Entry (self.info_frame, textvariable = self.host_var,
418 self.balloon.bind_widget (self.host, msg = dedented ("""
419 Host name of the local machine (e.g. 'mycomputer')
421 self.host.grid (row = 1, column = 1)
423 self.name_label = Label (self.info_frame)
424 self.name_label["text"] = "Full Name"
425 self.name_label.grid ()
427 self.name_var = StringVar ()
428 self.name_var.set (default_name ())
429 self.name_var.trace ("w", self.update_widgets)
431 self.name = Entry (self.info_frame, textvariable = self.name_var)
432 self.balloon.bind_widget (self.name, msg = dedented ("""
433 Full name as it should appear in the key description (e.g. 'John Doe')
435 self.name.grid (row = 2, column = 1)
437 self.email_label = Label (self.info_frame)
438 self.email_label["text"] = "Email address"
439 self.email_label.grid ()
441 self.email_var = StringVar ()
442 self.email_var.set (default_email ())
443 self.email_var.trace ("w", self.update_widgets)
445 self.email = Entry (self.info_frame, textvariable = self.email_var)
446 self.balloon.bind_widget (self.email, msg = dedented ("""
447 Email address associated with the name (e.g. '<test@example.com>')
449 self.email.grid (row = 3, column = 1)
451 self.comment_label = Label (self.info_frame)
452 self.comment_label["text"] = "Comment phrase"
453 self.comment_label.grid ()
455 self.comment_var = StringVar ()
456 self.comment_var.set (default_comment ())
457 self.comment_var.trace ("w", self.update_widgets)
459 self.comment = Entry (self.info_frame, textvariable = self.comment_var)
460 self.balloon.bind_widget (self.comment, msg = dedented ("""
461 Comment phrase for the GnuPG key, if any (e.g. 'key for 2014')
463 self.comment.grid (row = 4, column = 1)
465 self.options_frame = Frame (self)
466 self.options_frame.pack (fill = X)
468 self.gnupg_label = Label (self.options_frame)
469 self.gnupg_label["text"] = "Generate GnuPG key"
470 self.gnupg_label.grid ()
472 self.gnupg_var = IntVar ()
473 self.gnupg_var.set (1 if self.arguments.gnupg else 0)
474 self.gnupg_var.trace ("w", self.update_widgets)
476 self.gnupg = Checkbutton (self.options_frame,
477 variable = self.gnupg_var)
478 self.gnupg.grid (row = 0, column = 1)
480 self.openssh_label = Label (self.options_frame)
481 self.openssh_label["text"] = "Generate OpenSSH key"
482 self.openssh_label.grid ()
484 self.openssh_var = IntVar ()
485 self.openssh_var.set (1 if self.arguments.openssh else 0)
486 self.openssh_var.trace ("w", self.update_widgets)
488 self.openssh = Checkbutton (self.options_frame,
489 variable = self.openssh_var)
490 self.openssh.grid (row = 1, column = 1)
492 self.button_frame = Frame (self)
493 self.button_frame.pack (fill = X)
495 self._generate = Button (self.button_frame)
496 self._generate["text"] = "Generate Keys"
497 self._generate["command"] = self.generate
498 self.balloon.bind_widget (
500 msg = "Generate the keys as configured above")
501 self._generate.pack (side = LEFT, fill = Y)
503 self._quit = Button (self.button_frame)
504 self._quit["text"] = "Quit"
505 self._quit["command"] = self.quit
506 self.balloon.bind_widget (self._quit,
507 msg = "Quit the program immediately")
508 self._quit.pack (side = LEFT)
510 self.update_widgets ()
512 def valid_state (self):
513 if not self.openssh_var.get () and not self.gnupg_var.get ():
516 if gnupg_exists (self.arguments) and openssh_exists (self.arguments):
519 if not valid_email (self.email_var.get ()):
522 if not valid_name (self.name_var.get ()):
525 if not valid_comment (self.comment_var.get ()):
530 def update_widgets (self, *args):
531 valid = self.valid_state ()
533 self._generate["state"] = NORMAL if valid else DISABLED
535 name = self.name_var.get ()
537 valid = valid_name (name)
538 self.name["fg"] = "black" if valid else "red"
539 self.name_label["fg"] = "black" if valid else "red"
541 email = self.email_var.get ()
543 valid = valid_email (email)
544 self.email["fg"] = "black" if valid else "red"
545 self.email_label["fg"] = "black" if valid else "red"
547 comment = self.comment_var.get ()
549 valid = valid_comment (comment)
550 self.comment["fg"] = "black" if valid else "red"
551 self.comment_label["fg"] = "black" if valid else "red"
553 exists = gnupg_exists (self.arguments)
554 self.gnupg["state"] = NORMAL if not exists else DISABLED
556 exists = openssh_exists (self.arguments)
557 self.openssh["state"] = NORMAL if not exists else DISABLED
560 if comment.strip () != "":
561 gnupg_key + " ({}) ".format (comment)
562 gnupg_key += "<{}>".format (email)
564 user = self.user_var.get ()
565 host = self.host_var.get ()
567 openssh_key = "{}@{}".format (user, host)
570 Generate a GnuPG key for '{}' and configure a default setup for it
571 """).format (gnupg_key)
573 self.balloon.bind_widget (self.gnupg, msg = msg)
574 self.balloon.bind_widget (self.gnupg_label, msg = msg)
577 Generate an OpenSSH key for '{}' and configure a default setup for it
578 """).format (openssh_key)
580 self.balloon.bind_widget (self.openssh, msg = msg)
581 self.balloon.bind_widget (self.openssh_label, msg = msg)
583 def generate_thread (self):
587 sys.stdout = self.progress.redirect
589 # TODO: capture and show stdout and stderr
590 if self.gnupg_var.get ():
591 # TODO: make get calls thread-safe
592 gnupg_setup (self.arguments,
593 self.name_var.get (),
594 self.email_var.get (),
595 self.comment_var.get ())
596 # TODO: put update into queue
597 self.update_widgets ()
599 if self.openssh_var.get ():
600 comment = "{}@{}".format (self.user_var.get (),
601 self.host_var.get ())
602 openssh_setup (self.arguments, comment)
603 # TODO: put update into queue
604 self.update_widgets ()
611 self.progress.redirect.write (self.progress.queue.get (block = False))
616 self.progress = CryptoInstallProgress (self)
618 self.bind ("<<Idle>>", self._on_idle)
620 thread = threading.Thread (target = self.generate_thread)
625 arguments = parse_arguments ()
628 # TODO: use gtk instead? would be more consistent with the pinentry style
629 # (assuming it's using gtk)
630 CryptoInstall (arguments).mainloop ()
633 gnupg_setup (arguments)
635 if arguments.openssh:
636 openssh_setup (arguments)
639 if __name__ == "__main__":