2 # -*- mode: python; coding: utf-8; -*-
5 import argparse, errno, gettext, itertools, locale, os, re, readline, \
6 subprocess, 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 (*args, **kwargs):
22 gettext.install (*args, unicode = True, **kwargs)
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 gettext_install = gettext.install
37 raise Exception ("Unsupported Python version {}".format (sys.version_info))
41 return textwrap.dedent (text).strip ()
45 return textwrap.dedent (text).lstrip ()
49 return textwrap.fill (dedented (text), width = 72)
53 return textwrap.fill (ldedented (text), width = 72)
56 def read_input_string (prompt = "", default = ""):
58 readline.set_startup_hook (lambda: readline.insert_text (default))
61 return input_string(prompt)
63 readline.set_startup_hook()
66 def parse_arguments ():
67 parser = argparse.ArgumentParser ()
72 version = "crypto-install version GIT-TAG (GIT-COMMIT/GIT-BRANCH)",
73 help = _ ("Display version."))
77 action = "store_false",
78 help = _ ("Disable GUI, use text interface."))
79 gnupg_group = parser.add_argument_group (
81 _ ("Options related to the GnuPG setup."))
82 gnupg_group.add_argument (
85 action = "store_false",
86 help = _ ("Disable GnuPG setup."))
87 gnupg_group.add_argument (
90 default = os.getenv("GNUPGHOME") or "~/.gnupg",
92 help = _ ("Default directory for GnuPG files."))
93 openssh_group = parser.add_argument_group (
95 _ ("Options related to the OpenSSH setup."))
96 openssh_group.add_argument (
99 action = "store_false",
100 help = _ ("Disable OpenSSH setup."))
101 openssh_group.add_argument (
103 dest = "openssh_home",
105 metavar = _ ("PATH"),
106 help = _ ("Default directory for OpenSSH files."))
107 return parser.parse_args ()
110 def ensure_directories (path, mode = 0o777):
112 os.makedirs (path, mode)
113 except OSError as exception:
114 if exception.errno != errno.EEXIST:
119 return os.getenv ("FULLNAME")
122 def default_email ():
123 return os.getenv ("EMAIL")
126 def default_comment ():
130 def default_hostname ():
131 return subprocess.check_output ("hostname").strip ()
134 def default_username ():
135 return os.getenv ("USER")
138 def valid_email (value):
139 return re.match (".+@.+", value)
142 def valid_name (value):
143 return value.strip () != ""
146 def valid_user (value):
147 return value.strip () != ""
150 def valid_host (value):
151 return value.strip () != ""
154 def valid_comment (value):
158 def gnupg_exists (arguments):
159 gnupg_home = os.path.expanduser (arguments.gnupg_home)
160 gnupg_secring = os.path.join (gnupg_home, "secring.gpg")
162 return os.path.exists (gnupg_secring)
165 def openssh_exists (arguments):
166 openssh_home = os.path.expanduser (arguments.openssh_home)
167 openssh_config = os.path.join (openssh_home, "config")
168 openssh_key = os.path.join (openssh_home, "id_rsa")
170 return os.path.exists (openssh_config) and os.path.exists (openssh_key)
173 # TODO: verify phrase at least once
174 # TODO: use better labels
175 def input_passphrase (arguments):
176 batch_passphrase = ldedented ("""
180 """).format (subprocess.check_output ("tty").strip (),
183 batch_env = dict (os.environ)
185 batch_passphrase += ldedented ("""
188 """).format (os.getenv ("XAUTHORITY"),
189 os.getenv ("DISPLAY"))
191 del batch_env["DISPLAY"]
193 batch_passphrase += \
194 "GET_PASSPHRASE --data --check --qualitybar X X Passphrase X\n"
196 passphrase_process = subprocess.Popen (["gpg-agent", "--server"],
197 stdin = subprocess.PIPE,
198 stdout = subprocess.PIPE,
199 stderr = subprocess.PIPE,
201 (stdout, stderr) = passphrase_process.communicate (batch_passphrase.encode ("UTF-8"))
203 if passphrase_process.returncode != 0:
204 raise Exception ("Couldn't read passphrase.")
206 for line in stdout.splitlines ():
207 if line.decode ("UTF-8").startswith ("D "):
213 def redirect_to_stdout (process):
214 # TODO: argh. there has to be a better way
215 process.stdin.close ()
216 while process.poll () is None:
217 sys.stdout.write (process.stdout.readline ())
220 line = process.stdout.readline ()
223 sys.stdout.write (line.decode ("UTF-8"))
226 def gnupg_setup (arguments, name = None, email = None, comment = None):
227 gnupg_home = os.path.expanduser (arguments.gnupg_home)
228 gnupg_secring = os.path.join (gnupg_home, "secring.gpg")
230 if gnupg_exists (arguments):
231 print (_ ("GnuPG secret keyring already exists at '{}'.")
232 .format (gnupg_secring))
235 if not arguments.gui:
236 print (filled (_ ("""
237 No default GnuPG key available. Please enter your information to
238 create a new key.""")))
240 name = read_input_string (_ ("What is your name (e.g. 'John Doe')? "),
243 email = read_input_string (dedented (_ ("""
244 What is your email address (e.g. 'test@example.com')? """)),
247 comment = read_input_string (dedented (_ ("""
248 What is your comment phrase, if any (e.g. 'key for 2014')? """)),
251 if not os.path.exists (gnupg_home):
252 print (_ ("Creating GnuPG directory at '{}'.").format (gnupg_home))
253 ensure_directories (gnupg_home, 0o700)
255 with tempfile.NamedTemporaryFile () as tmp:
256 batch_key = ldedented ("""
265 """).format (name, email)
268 batch_key += "Name-Comment: {}\n".format (comment)
270 tmp.write (batch_key.encode ("UTF-8"))
273 batch_env = dict (os.environ)
274 if not arguments.gui:
275 del batch_env["DISPLAY"]
277 gnupg_process = subprocess.Popen (["gpg2",
278 "--homedir", gnupg_home,
279 "--batch", "--gen-key", tmp.name],
280 stdin = subprocess.PIPE,
281 stdout = subprocess.PIPE,
282 stderr = subprocess.STDOUT,
285 redirect_to_stdout (gnupg_process)
287 if gnupg_process.returncode != 0:
288 raise Exception ("Couldn't create GnuPG key.")
291 def openssh_setup (arguments, comment = None):
292 openssh_home = os.path.expanduser (arguments.openssh_home)
293 openssh_config = os.path.join (openssh_home, "config")
295 if not os.path.exists (openssh_config):
296 print (_ ("Creating OpenSSH directory at '{}'.").format (openssh_home))
297 ensure_directories (openssh_home, 0o700)
299 print (_ ("Creating OpenSSH configuration at '{}'.")
300 .format (openssh_config))
301 with open (openssh_config, "w") as config:
302 config.write (ldedented ("""
307 openssh_key = os.path.join (openssh_home, "id_rsa")
309 if os.path.exists (openssh_key):
310 print (_ ("OpenSSH key already exists at '{}'.").format (openssh_key))
313 openssh_key_dsa = os.path.join (openssh_home, "id_dsa")
315 if os.path.exists (openssh_key_dsa):
316 print (_ ("OpenSSH key already exists at '{}'.").format (openssh_key_dsa))
319 print (filled (_ ("No OpenSSH key available. Generating new key.")))
321 if not arguments.gui:
322 comment = "{}@{}".format (default_username (), default_hostname ())
323 comment = read_input_string (ldedented (_ ("""
324 What is your comment phrase (e.g. 'user@mycomputer')? """)), comment)
326 passphrase = input_passphrase (arguments)
328 batch_env = dict (os.environ)
329 if not arguments.gui:
330 del batch_env["DISPLAY"]
332 # TODO: is it somehow possible to pass the password on stdin?
333 openssh_process = subprocess.Popen (["ssh-keygen",
337 stdin = subprocess.PIPE,
338 stdout = subprocess.PIPE,
339 stderr = subprocess.STDOUT,
342 redirect_to_stdout (openssh_process)
344 if openssh_process.returncode != 0:
345 raise Exception ("Couldn't create OpenSSH key.")
349 return NORMAL if value else DISABLED
353 return "black" if value else "red"
356 def setitem (object, name, value):
357 return object.__setitem__ (name, value)
360 def setitems (name, value, objects):
361 return map (lambda object: setitem (object, name, value), objects)
364 def _fg (value, *objects):
365 setitems ("fg", value, objects)
368 # http://www.blog.pythonlibrary.org/2014/07/14/tkinter-redirecting-stdout-stderr/
369 # http://www.virtualroadside.com/blog/index.php/2012/11/10/glib-idle_add-for-tkinter-in-python/
370 class RedirectText (object):
371 def __init__ (self, root, widget):
375 self.queue = Queue ()
377 def write (self, string):
378 self.widget.insert (END, string)
380 def enqueue (self, value):
381 self.queue.put (value)
382 self.root.event_generate ("<<Idle>>", when = "tail")
385 class CryptoInstallProgress (Toplevel):
386 def __init__ (self, parent):
387 Toplevel.__init__ (self, parent)
391 self.create_widgets ()
393 def create_widgets (self):
394 self.balloon = Balloon (self, initwait = 250)
396 self.text = ScrolledText (self)
397 self.text.pack (fill = BOTH, expand = True)
399 self.redirect = RedirectText (self.parent, self.text)
401 self._quit = Button (self,
404 self.balloon.bind_widget (self._quit,
405 msg = _ ("Quit the program immediately"))
409 class CryptoInstall (Tk):
410 def __init__ (self, arguments):
413 self.arguments = arguments
415 self.resizable (width = False, height = False)
416 self.title (_ ("Crypto Install Wizard"))
418 self.create_widgets ()
420 def create_widgets (self):
423 self.balloon = Balloon (self, initwait = 250)
425 self.info_frame = Frame (self)
426 self.info_frame.pack (fill = X)
428 msg = dedented (_ ("""
429 Username on the local machine (e.g. 'user')
431 self.user_label = Label (self.info_frame, text = _ ("Username"))
432 self.balloon.bind_widget (self.user_label, msg = msg)
433 self.user_label.grid ()
435 self.user_var = StringVar (self, default_username ())
436 self.user_var.trace ("w", self.update_widgets)
438 self.user = Entry (self.info_frame, textvariable = self.user_var)
439 self.balloon.bind_widget (self.user, msg = msg)
440 self.user.grid (row = 0, column = 1)
442 self.fields["user"] = [self.user_var, valid_user,
443 self.user, self.user_label]
445 msg = dedented (_ ("""
446 Host name of the local machine (e.g. 'mycomputer')
448 self.host_label = Label (self.info_frame, text = _ ("Host Name"))
449 self.balloon.bind_widget (self.host_label, msg = msg)
450 self.host_label.grid ()
452 self.host_var = StringVar (self, default_hostname ())
453 self.host_var.trace ("w", self.update_widgets)
455 self.host = Entry (self.info_frame, textvariable = self.host_var)
456 self.balloon.bind_widget (self.host, msg = msg)
457 self.host.grid (row = 1, column = 1)
459 self.fields["host"] = [self.host_var, valid_host,
460 self.host, self.host_label]
462 msg = dedented (_ ("""
463 Full name as it should appear in the key description (e.g. 'John Doe')
465 self.name_label = Label (self.info_frame, text = _ ("Full Name"))
466 self.balloon.bind_widget (self.name_label, msg = msg)
467 self.name_label.grid ()
469 self.name_var = StringVar (self, default_name ())
470 self.name_var.trace ("w", self.update_widgets)
472 self.name = Entry (self.info_frame, textvariable = self.name_var)
473 self.balloon.bind_widget (self.name, msg = msg)
474 self.name.grid (row = 2, column = 1)
476 self.fields["name"] = [self.name_var, valid_name,
477 self.name, self.name_label]
479 msg = dedented (_ ("""
480 Email address associated with the name (e.g. '<test@example.com>')
482 self.email_label = Label (self.info_frame, text = _ ("Email address"))
483 self.balloon.bind_widget (self.email_label, msg = msg)
484 self.email_label.grid ()
486 self.email_var = StringVar (self, default_email ())
487 self.email_var.trace ("w", self.update_widgets)
489 self.email = Entry (self.info_frame, textvariable = self.email_var)
490 self.balloon.bind_widget (self.email, msg = msg)
491 self.email.grid (row = 3, column = 1)
493 self.fields["email"] = [self.email_var, valid_email,
494 self.email, self.email_label]
496 msg = dedented (_ ("""
497 Comment phrase for the GnuPG key, if any (e.g. 'key for 2014')
499 self.comment_label = Label (self.info_frame, text = _ ("Comment phrase"))
500 self.balloon.bind_widget (self.comment_label, msg = msg)
501 self.comment_label.grid ()
503 self.comment_var = StringVar (self, default_comment ())
504 self.comment_var.trace ("w", self.update_widgets)
506 self.comment = Entry (self.info_frame, textvariable = self.comment_var)
507 self.balloon.bind_widget (self.comment, msg = msg)
508 self.comment.grid (row = 4, column = 1)
510 self.fields["comment"] = [self.comment_var, valid_comment,
511 self.comment, self.comment_label]
513 self.options_frame = Frame (self)
514 self.options_frame.pack (fill = X)
516 self.gnupg_label = Label (self.options_frame,
517 text = _ ("Generate GnuPG key"))
518 self.gnupg_label.grid ()
520 self.gnupg_var = IntVar (self, 1 if self.arguments.gnupg else 0)
521 self.gnupg_var.trace ("w", self.update_widgets)
523 self.gnupg = Checkbutton (self.options_frame,
524 variable = self.gnupg_var)
525 self.gnupg.grid (row = 0, column = 1)
527 self.openssh_label = Label (self.options_frame,
528 text = _ ("Generate OpenSSH key"))
529 self.openssh_label.grid ()
531 self.openssh_var = IntVar (self, 1 if self.arguments.openssh else 0)
532 self.openssh_var.trace ("w", self.update_widgets)
534 self.openssh = Checkbutton (self.options_frame,
535 variable = self.openssh_var)
536 self.openssh.grid (row = 1, column = 1)
538 self.button_frame = Frame (self)
539 self.button_frame.pack (fill = X)
541 self._generate = Button (self.button_frame, text = _ ("Generate Keys"),
542 command = self.generate)
543 self.balloon.bind_widget (
545 msg = _ ("Generate the keys as configured above"))
546 self._generate.pack (side = LEFT, fill = Y)
548 self._quit = Button (self.button_frame, text = _ ("Quit"),
550 self.balloon.bind_widget (self._quit,
551 msg = _ ("Quit the program immediately"))
552 self._quit.pack (side = LEFT)
554 self.update_widgets ()
556 def valid_state (self):
557 if not self.openssh_var.get () and not self.gnupg_var.get ():
560 if gnupg_exists (self.arguments) and openssh_exists (self.arguments):
563 if not valid_name (self.user_var.get ()):
566 if not valid_host (self.host_var.get ()):
569 if not valid_email (self.email_var.get ()):
572 if not valid_name (self.name_var.get ()):
575 if not valid_comment (self.comment_var.get ()):
580 def update_field (self, name):
581 field = self.fields[name]
583 _fg (_valid (field[1] (field[0].get ())), field[2], field[3])
585 def update_widgets (self, *args):
586 self._generate["state"] = _state (self.valid_state ())
588 for field in ["user", "host", "name", "email", "comment"]:
589 self.update_field (field)
591 self.gnupg["state"] = _state (not gnupg_exists (self.arguments))
592 self.openssh["state"] = _state (not openssh_exists (self.arguments))
594 gnupg_key = self.name_var.get ().strip ()
595 comment = self.comment_var.get ().strip ()
597 gnupg_key + " ({}) ".format (comment)
598 gnupg_key += "<{}>".format (self.email_var.get ().strip ())
600 user = self.user_var.get ().strip ()
601 host = self.host_var.get ().strip ()
603 openssh_key = "{}@{}".format (user, host)
605 msg = dedented (_ ("""
606 Generate a GnuPG key for '{}' and configure a default setup for it
607 """)).format (gnupg_key)
609 self.balloon.bind_widget (self.gnupg, msg = msg)
610 self.balloon.bind_widget (self.gnupg_label, msg = msg)
612 msg = dedented (_ ("""
613 Generate an OpenSSH key for '{}' and configure a default setup for it
614 """)).format (openssh_key)
616 self.balloon.bind_widget (self.openssh, msg = msg)
617 self.balloon.bind_widget (self.openssh_label, msg = msg)
619 def generate_thread (self, gnupg, openssh, name, email, comment, user,
624 sys.stdout = self.progress.redirect
627 gnupg_setup (self.arguments, name, email, comment)
628 # TODO: put update into queue
629 self.update_widgets ()
632 comment = "{}@{}".format (user, host)
633 openssh_setup (self.arguments, comment)
634 # TODO: put update into queue
635 self.update_widgets ()
642 message = self.progress.queue.get (block = False)
643 self.progress.redirect.write (message)
648 self.progress = CryptoInstallProgress (self)
650 self.bind ("<<Idle>>", self._on_idle)
652 thread = threading.Thread (target = self.generate_thread,
653 args = (self.gnupg_var.get (),
654 self.openssh_var.get (),
655 self.name_var.get (),
656 self.email_var.get (),
657 self.comment_var.get (),
658 self.user_var.get (),
659 self.host_var.get ()))
664 locale.setlocale (locale.LC_ALL, "")
666 gettext_install ("crypto-install", localedir = os.getenv ("TEXTDOMAINDIR"))
668 arguments = parse_arguments ()
671 # TODO: use gtk instead? would be more consistent with the pinentry style
672 # (assuming it's using gtk)
673 CryptoInstall (arguments).mainloop ()
676 gnupg_setup (arguments)
678 if arguments.openssh:
679 openssh_setup (arguments)
682 if __name__ == "__main__":