2 # -*- mode: python; coding: utf-8; -*-
5 import argparse, errno, gettext, itertools, os, re, readline, subprocess, \
6 sys, tempfile, textwrap, threading
9 if sys.version_info[0] == 2:
11 from tkMessageBox import *
13 from ScrolledText import *
17 def input_string (prompt=""):
18 return raw_input (prompt)
21 def gettext_install ():
22 gettext.install ("crypto-install", unicode = True)
23 elif sys.version_info[0] > 2:
25 from tkinter.messagebox import *
26 from tkinter.tix import *
27 from tkinter.scrolledtext import *
31 def input_string (prompt=""):
35 def gettext_install ():
36 gettext.install ("crypto-install")
38 raise Exception ("Unsupported Python version {}".format (sys.version_info))
42 return textwrap.dedent (text).strip ()
46 return textwrap.dedent (text).lstrip ()
50 return textwrap.fill (dedented (text), width = 72)
54 return textwrap.fill (ldedented (text), width = 72)
57 def read_input_string (prompt = "", default = ""):
59 readline.set_startup_hook (lambda: readline.insert_text (default))
62 return input_string(prompt)
64 readline.set_startup_hook()
67 def parse_arguments ():
68 parser = argparse.ArgumentParser ()
73 version = "crypto-install.py version GIT-TAG (GIT-COMMIT/GIT-BRANCH)",
74 help = _ ("Display version."))
78 action = "store_false",
79 help = _ ("Disable GUI, use text interface."))
80 gnupg_group = parser.add_argument_group (
82 _ ("Options related to the GnuPG setup."))
83 gnupg_group.add_argument (
86 action = "store_false",
87 help = "Disable GnuPG setup.")
88 gnupg_group.add_argument (
91 default = os.getenv("GNUPGHOME") or "~/.gnupg",
93 help = _ ("Default directory for GnuPG files."))
94 openssh_group = parser.add_argument_group (
96 _ ("Options related to the OpenSSH setup."))
97 openssh_group.add_argument (
100 action = "store_false",
101 help = _ ("Disable OpenSSH setup."))
102 openssh_group.add_argument (
104 dest = "openssh_home",
107 help = _ ("Default directory for OpenSSH files."))
108 return parser.parse_args ()
111 def ensure_directories (path, mode = 0o777):
113 os.makedirs (path, mode)
114 except OSError as exception:
115 if exception.errno != errno.EEXIST:
120 return os.getenv ("FULLNAME")
123 def default_email ():
124 return os.getenv ("EMAIL")
127 def default_comment ():
131 def default_hostname ():
132 return subprocess.check_output ("hostname").strip ()
135 def default_username ():
136 return os.getenv ("USER")
139 def valid_email (value):
140 return re.match (".+@.+", value)
143 def valid_name (value):
144 return value.strip () != ""
147 def valid_user (value):
148 return value.strip () != ""
151 def valid_host (value):
152 return value.strip () != ""
155 def valid_comment (value):
159 def gnupg_exists (arguments):
160 gnupg_home = os.path.expanduser (arguments.gnupg_home)
161 gnupg_secring = os.path.join (gnupg_home, "secring.gpg")
163 return os.path.exists (gnupg_secring)
166 def openssh_exists (arguments):
167 openssh_home = os.path.expanduser (arguments.openssh_home)
168 openssh_config = os.path.join (openssh_home, "config")
169 openssh_key = os.path.join (openssh_home, "id_rsa")
171 return os.path.exists (openssh_config) and os.path.exists (openssh_key)
174 # TODO: verify phrase at least once
175 # TODO: use better labels
176 def input_passphrase (arguments):
177 batch_passphrase = ldedented ("""
181 """).format (subprocess.check_output ("tty").strip (),
184 batch_env = dict (os.environ)
186 batch_passphrase += ldedented ("""
189 """).format (os.getenv ("XAUTHORITY"),
190 os.getenv ("DISPLAY"))
192 del batch_env["DISPLAY"]
194 batch_passphrase += \
195 "GET_PASSPHRASE --data --check --qualitybar X X Passphrase X\n"
197 passphrase_process = subprocess.Popen (["gpg-agent", "--server"],
198 stdin = subprocess.PIPE,
199 stdout = subprocess.PIPE,
200 stderr = subprocess.PIPE,
202 (stdout, stderr) = passphrase_process.communicate (batch_passphrase.encode ("UTF-8"))
204 if passphrase_process.returncode != 0:
205 raise Exception ("Couldn't read passphrase.")
207 for line in stdout.splitlines ():
208 if line.decode ("UTF-8").startswith ("D "):
214 def redirect_to_stdout (process):
215 # TODO: argh. there has to be a better way
216 process.stdin.close ()
217 while process.poll () is None:
218 sys.stdout.write (process.stdout.readline ())
221 line = process.stdout.readline ()
224 sys.stdout.write (line.decode ("UTF-8"))
227 def gnupg_setup (arguments, name = None, email = None, comment = None):
228 gnupg_home = os.path.expanduser (arguments.gnupg_home)
229 gnupg_secring = os.path.join (gnupg_home, "secring.gpg")
231 if gnupg_exists (arguments):
232 print (_ ("GnuPG secret keyring already exists at '{}'.")
233 .format (gnupg_secring))
236 if not arguments.gui:
237 print (filled (_ ("""
238 No default GnuPG key available. Please enter your information to
239 create a new key.""")))
241 name = read_input_string (_ ("What is your name (e.g. 'John Doe')? "),
244 email = read_input_string (dedented (_ ("""
245 What is your email address (e.g. 'test@example.com')? """)),
248 comment = read_input_string (dedented (_ ("""
249 What is your comment phrase, if any (e.g. 'key for 2014')? """)),
252 if not os.path.exists (gnupg_home):
253 print (_ ("Creating GnuPG directory at '{}'.").format (gnupg_home))
254 ensure_directories (gnupg_home, 0o700)
256 with tempfile.NamedTemporaryFile () as tmp:
257 batch_key = ldedented ("""
266 """).format (name, email)
269 batch_key += "Name-Comment: {}\n".format (comment)
271 tmp.write (batch_key.encode ("UTF-8"))
274 batch_env = dict (os.environ)
275 if not arguments.gui:
276 del batch_env["DISPLAY"]
278 gnupg_process = subprocess.Popen (["gpg2",
279 "--homedir", gnupg_home,
280 "--batch", "--gen-key", tmp.name],
281 stdin = subprocess.PIPE,
282 stdout = subprocess.PIPE,
283 stderr = subprocess.STDOUT,
286 redirect_to_stdout (gnupg_process)
288 if gnupg_process.returncode != 0:
289 raise Exception ("Couldn't create GnuPG key.")
292 def openssh_setup (arguments, comment = None):
293 openssh_home = os.path.expanduser (arguments.openssh_home)
294 openssh_config = os.path.join (openssh_home, "config")
296 if not os.path.exists (openssh_config):
297 print (_ ("Creating OpenSSH directory at '{}'.").format (openssh_home))
298 ensure_directories (openssh_home, 0o700)
300 print (_ ("Creating OpenSSH configuration at '{}'.")
301 .format (openssh_config))
302 with open (openssh_config, "w") as config:
303 config.write (ldedented ("""
308 openssh_key = os.path.join (openssh_home, "id_rsa")
310 if os.path.exists (openssh_key):
311 print (_ ("OpenSSH key already exists at '{}'.").format (openssh_key))
314 openssh_key_dsa = os.path.join (openssh_home, "id_dsa")
316 if os.path.exists (openssh_key_dsa):
317 print (_ ("OpenSSH key already exists at '{}'.").format (openssh_key_dsa))
320 print (filled (_ ("No OpenSSH key available. Generating new key.")))
322 if not arguments.gui:
323 comment = "{}@{}".format (default_username (), default_hostname ())
324 comment = read_input_string (ldedented (_ ("""
325 What is your comment phrase (e.g. 'user@mycomputer')? """)), comment)
327 passphrase = input_passphrase (arguments)
329 batch_env = dict (os.environ)
330 if not arguments.gui:
331 del batch_env["DISPLAY"]
333 # TODO: is it somehow possible to pass the password on stdin?
334 openssh_process = subprocess.Popen (["ssh-keygen",
338 stdin = subprocess.PIPE,
339 stdout = subprocess.PIPE,
340 stderr = subprocess.STDOUT,
343 redirect_to_stdout (openssh_process)
345 if openssh_process.returncode != 0:
346 raise Exception ("Couldn't create OpenSSH key.")
350 return NORMAL if value else DISABLED
354 return "black" if value else "red"
357 def setitem (object, name, value):
358 return object.__setitem__ (name, value)
361 def setitems (name, value, objects):
362 return map (lambda object: setitem (object, name, value), objects)
365 def _fg (value, *objects):
366 setitems ("fg", value, objects)
369 # http://www.blog.pythonlibrary.org/2014/07/14/tkinter-redirecting-stdout-stderr/
370 # http://www.virtualroadside.com/blog/index.php/2012/11/10/glib-idle_add-for-tkinter-in-python/
371 class RedirectText (object):
372 def __init__ (self, root, widget):
376 self.queue = Queue ()
378 def write (self, string):
379 self.widget.insert (END, string)
381 def enqueue (self, value):
382 self.queue.put (value)
383 self.root.event_generate ("<<Idle>>", when = "tail")
386 class CryptoInstallProgress (Toplevel):
387 def __init__ (self, parent):
388 Toplevel.__init__ (self, parent)
392 self.create_widgets ()
394 def create_widgets (self):
395 self.balloon = Balloon (self, initwait = 250)
397 self.text = ScrolledText (self)
398 self.text.pack (fill = BOTH, expand = True)
400 self.redirect = RedirectText (self.parent, self.text)
402 self._quit = Button (self,
405 self.balloon.bind_widget (self._quit,
406 msg = _ ("Quit the program immediately"))
410 class CryptoInstall (Tk):
411 def __init__ (self, arguments):
414 self.arguments = arguments
416 self.resizable (width = False, height = False)
417 self.title (_ ("Crypto Install Wizard"))
419 self.create_widgets ()
421 def create_widgets (self):
424 self.balloon = Balloon (self, initwait = 250)
426 self.info_frame = Frame (self)
427 self.info_frame.pack (fill = X)
429 self.user_label = Label (self.info_frame, text = ("Username"))
430 self.user_label.grid ()
432 self.user_var = StringVar (self, default_username ())
433 self.user_var.trace ("w", self.update_widgets)
435 self.user = Entry (self.info_frame, textvariable = self.user_var)
436 self.balloon.bind_widget (self.user, msg = dedented (_ ("""
437 Username on the local machine (e.g. 'user')
439 self.user.grid (row = 0, column = 1)
441 self.fields["user"] = [self.user_var, valid_user,
442 self.user, self.user_label]
444 self.host_label = Label (self.info_frame, text = _ ("Host Name"))
445 self.host_label.grid ()
447 self.host_var = StringVar (self, default_hostname ())
448 self.host_var.trace ("w", self.update_widgets)
450 self.host = Entry (self.info_frame, textvariable = self.host_var)
451 self.balloon.bind_widget (self.host, msg = dedented (_ ("""
452 Host name of the local machine (e.g. 'mycomputer')
454 self.host.grid (row = 1, column = 1)
456 self.fields["host"] = [self.host_var, valid_host,
457 self.host, self.host_label]
459 self.name_label = Label (self.info_frame, text = _ ("Full Name"))
460 self.name_label.grid ()
462 self.name_var = StringVar (self, default_name ())
463 self.name_var.trace ("w", self.update_widgets)
465 self.name = Entry (self.info_frame, textvariable = self.name_var)
466 self.balloon.bind_widget (self.name, msg = dedented (_ ("""
467 Full name as it should appear in the key description (e.g. 'John Doe')
469 self.name.grid (row = 2, column = 1)
471 self.fields["name"] = [self.name_var, valid_name,
472 self.name, self.name_label]
474 self.email_label = Label (self.info_frame, text = _ ("Email address"))
475 self.email_label.grid ()
477 self.email_var = StringVar (self, default_email ())
478 self.email_var.trace ("w", self.update_widgets)
480 self.email = Entry (self.info_frame, textvariable = self.email_var)
481 self.balloon.bind_widget (self.email, msg = dedented (_ ("""
482 Email address associated with the name (e.g. '<test@example.com>')
484 self.email.grid (row = 3, column = 1)
486 self.fields["email"] = [self.email_var, valid_email,
487 self.email, self.email_label]
489 self.comment_label = Label (self.info_frame, text = _ ("Comment phrase"))
490 self.comment_label.grid ()
492 self.comment_var = StringVar (self, default_comment ())
493 self.comment_var.trace ("w", self.update_widgets)
495 self.comment = Entry (self.info_frame, textvariable = self.comment_var)
496 self.balloon.bind_widget (self.comment, msg = dedented (_ ("""
497 Comment phrase for the GnuPG key, if any (e.g. 'key for 2014')
499 self.comment.grid (row = 4, column = 1)
501 self.fields["comment"] = [self.comment_var, valid_comment,
502 self.comment, self.comment_label]
504 self.options_frame = Frame (self)
505 self.options_frame.pack (fill = X)
507 self.gnupg_label = Label (self.options_frame,
508 text = _ ("Generate GnuPG key"))
509 self.gnupg_label.grid ()
511 self.gnupg_var = IntVar (self, 1 if self.arguments.gnupg else 0)
512 self.gnupg_var.trace ("w", self.update_widgets)
514 self.gnupg = Checkbutton (self.options_frame,
515 variable = self.gnupg_var)
516 self.gnupg.grid (row = 0, column = 1)
518 self.openssh_label = Label (self.options_frame,
519 text = _ ("Generate OpenSSH key"))
520 self.openssh_label.grid ()
522 self.openssh_var = IntVar (self, 1 if self.arguments.openssh else 0)
523 self.openssh_var.trace ("w", self.update_widgets)
525 self.openssh = Checkbutton (self.options_frame,
526 variable = self.openssh_var)
527 self.openssh.grid (row = 1, column = 1)
529 self.button_frame = Frame (self)
530 self.button_frame.pack (fill = X)
532 self._generate = Button (self.button_frame, text = _ ("Generate Keys"),
533 command = self.generate)
534 self.balloon.bind_widget (
536 msg = _ ("Generate the keys as configured above"))
537 self._generate.pack (side = LEFT, fill = Y)
539 self._quit = Button (self.button_frame, text = _ ("Quit"),
541 self.balloon.bind_widget (self._quit,
542 msg = _ ("Quit the program immediately"))
543 self._quit.pack (side = LEFT)
545 self.update_widgets ()
547 def valid_state (self):
548 if not self.openssh_var.get () and not self.gnupg_var.get ():
551 if gnupg_exists (self.arguments) and openssh_exists (self.arguments):
554 if not valid_name (self.user_var.get ()):
557 if not valid_host (self.host_var.get ()):
560 if not valid_email (self.email_var.get ()):
563 if not valid_name (self.name_var.get ()):
566 if not valid_comment (self.comment_var.get ()):
571 def update_field (self, name):
572 field = self.fields[name]
574 _fg (_valid (field[1] (field[0].get ())), field[2], field[3])
576 def update_widgets (self, *args):
577 self._generate["state"] = _state (self.valid_state ())
579 for field in ["user", "host", "name", "email", "comment"]:
580 self.update_field (field)
582 self.gnupg["state"] = _state (not gnupg_exists (self.arguments))
583 self.openssh["state"] = _state (not openssh_exists (self.arguments))
585 gnupg_key = self.name_var.get ().strip ()
586 comment = self.comment_var.get ().strip ()
588 gnupg_key + " ({}) ".format (comment)
589 gnupg_key += "<{}>".format (self.email_var.get ().strip ())
591 user = self.user_var.get ().strip ()
592 host = self.host_var.get ().strip ()
594 openssh_key = "{}@{}".format (user, host)
596 msg = dedented (_ ("""
597 Generate a GnuPG key for '{}' and configure a default setup for it
598 """)).format (gnupg_key)
600 self.balloon.bind_widget (self.gnupg, msg = msg)
601 self.balloon.bind_widget (self.gnupg_label, msg = msg)
603 msg = dedented (_ ("""
604 Generate an OpenSSH key for '{}' and configure a default setup for it
605 """)).format (openssh_key)
607 self.balloon.bind_widget (self.openssh, msg = msg)
608 self.balloon.bind_widget (self.openssh_label, msg = msg)
610 def generate_thread (self):
614 sys.stdout = self.progress.redirect
616 # TODO: capture and show stdout and stderr
617 if self.gnupg_var.get ():
618 # TODO: make get calls thread-safe
619 gnupg_setup (self.arguments,
620 self.name_var.get (),
621 self.email_var.get (),
622 self.comment_var.get ())
623 # TODO: put update into queue
624 self.update_widgets ()
626 if self.openssh_var.get ():
627 comment = "{}@{}".format (self.user_var.get (),
628 self.host_var.get ())
629 openssh_setup (self.arguments, comment)
630 # TODO: put update into queue
631 self.update_widgets ()
638 self.progress.redirect.write (self.progress.queue.get (block = False))
643 self.progress = CryptoInstallProgress (self)
645 self.bind ("<<Idle>>", self._on_idle)
647 thread = threading.Thread (target = self.generate_thread)
654 arguments = parse_arguments ()
657 # TODO: use gtk instead? would be more consistent with the pinentry style
658 # (assuming it's using gtk)
659 CryptoInstall (arguments).mainloop ()
662 gnupg_setup (arguments)
664 if arguments.openssh:
665 openssh_setup (arguments)
668 if __name__ == "__main__":