2 # -*- mode: python; coding: utf-8; -*-
5 import argparse, errno, gettext, os, re, readline, subprocess, sys, tempfile, textwrap, threading
8 if sys.version_info[0] == 2:
10 from tkMessageBox import *
12 from ScrolledText import *
16 def input_string (prompt=""):
17 return raw_input (prompt)
20 def gettext_install ():
21 gettext.install ("crypto-install", unicode = True)
22 elif sys.version_info[0] > 2:
24 from tkinter.messagebox import *
25 from tkinter.tix import *
26 from tkinter.scrolledtext import *
30 def input_string (prompt=""):
34 def gettext_install ():
35 gettext.install ("crypto-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.py 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",
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_comment (value):
150 def gnupg_exists (arguments):
151 gnupg_home = os.path.expanduser (arguments.gnupg_home)
152 gnupg_secring = os.path.join (gnupg_home, "secring.gpg")
154 return os.path.exists (gnupg_secring)
157 def openssh_exists (arguments):
158 openssh_home = os.path.expanduser (arguments.openssh_home)
159 openssh_config = os.path.join (openssh_home, "config")
160 openssh_key = os.path.join (openssh_home, "id_rsa")
162 return os.path.exists (openssh_config) and os.path.exists (openssh_key)
165 # TODO: verify phrase at least once
166 # TODO: use better labels
167 def input_passphrase (arguments):
168 batch_passphrase = ldedented ("""
172 """).format (subprocess.check_output ("tty").strip (),
175 batch_env = dict (os.environ)
177 batch_passphrase += ldedented ("""
180 """).format (os.getenv ("XAUTHORITY"),
181 os.getenv ("DISPLAY"))
183 del batch_env["DISPLAY"]
185 batch_passphrase += \
186 "GET_PASSPHRASE --data --check --qualitybar X X Passphrase X\n"
188 passphrase_process = subprocess.Popen (["gpg-agent", "--server"],
189 stdin = subprocess.PIPE,
190 stdout = subprocess.PIPE,
191 stderr = subprocess.PIPE,
193 (stdout, stderr) = passphrase_process.communicate (batch_passphrase.encode ("UTF-8"))
195 if passphrase_process.returncode != 0:
196 raise Exception ("Couldn't read passphrase.")
198 for line in stdout.splitlines ():
199 if line.decode ("UTF-8").startswith ("D "):
205 def gnupg_setup (arguments, name = None, email = None, comment = None):
206 gnupg_home = os.path.expanduser (arguments.gnupg_home)
207 gnupg_secring = os.path.join (gnupg_home, "secring.gpg")
209 if gnupg_exists (arguments):
210 print (_ ("GnuPG secret keyring already exists at '{}'.")
211 .format (gnupg_secring))
214 if not arguments.gui:
215 print (filled (_ ("""
216 No default GnuPG key available. Please enter your information to
217 create a new key.""")))
219 name = read_input_string (_ ("What is your name (e.g. 'John Doe')? "),
222 email = read_input_string (dedented (_ ("""
223 What is your email address (e.g. 'test@example.com')? """)),
226 comment = read_input_string (dedented (_ ("""
227 What is your comment phrase, if any (e.g. 'key for 2014')? """)),
230 if not os.path.exists (gnupg_home):
231 print (_ ("Creating GnuPG directory at '{}'.").format (gnupg_home))
232 ensure_directories (gnupg_home, 0o700)
234 with tempfile.NamedTemporaryFile () as tmp:
235 batch_key = ldedented ("""
244 """).format (name, email)
247 batch_key += "Name-Comment: {}\n".format (comment)
249 tmp.write (batch_key.encode ("UTF-8"))
252 batch_env = dict (os.environ)
253 if not arguments.gui:
254 del batch_env["DISPLAY"]
256 gnupg_process = subprocess.Popen (["gpg2",
257 "--homedir", gnupg_home,
258 "--batch", "--gen-key", tmp.name],
259 stdin = subprocess.PIPE,
260 stdout = subprocess.PIPE,
261 stderr = subprocess.STDOUT,
264 # TODO: argh. there has to be a better way
265 gnupg_process.stdin.close ()
266 while gnupg_process.poll () is None:
267 sys.stdout.write (gnupg_process.stdout.readline ())
270 line = gnupg_process.stdout.readline ()
273 sys.stdout.write (line.decode ("UTF-8"))
275 if gnupg_process.returncode != 0:
276 raise Exception ("Couldn't create GnuPG key.")
279 def openssh_setup (arguments, comment = None):
280 openssh_home = os.path.expanduser (arguments.openssh_home)
281 openssh_config = os.path.join (openssh_home, "config")
283 if not os.path.exists (openssh_config):
284 print (_ ("Creating OpenSSH directory at '{}'.").format (openssh_home))
285 ensure_directories (openssh_home, 0o700)
287 print (_ ("Creating OpenSSH configuration at '{}'.")
288 .format (openssh_config))
289 with open (openssh_config, "w") as config:
290 config.write (ldedented ("""
295 openssh_key = os.path.join (openssh_home, "id_rsa")
297 if os.path.exists (openssh_key):
298 print (_ ("OpenSSH key already exists at '{}'.").format (openssh_key))
301 openssh_key_dsa = os.path.join (openssh_home, "id_dsa")
303 if os.path.exists (openssh_key_dsa):
304 print (_ ("OpenSSH key already exists at '{}'.").format (openssh_key_dsa))
307 print (filled (_ ("No OpenSSH key available. Generating new key.")))
309 if not arguments.gui:
310 comment = "{}@{}".format (default_username (), default_hostname ())
311 comment = read_input_string (ldedented (_ ("""
312 What is your comment phrase (e.g. 'user@mycomputer')? """)), comment)
314 passphrase = input_passphrase (arguments)
316 batch_env = dict (os.environ)
317 if not arguments.gui:
318 del batch_env["DISPLAY"]
320 # TODO: is it somehow possible to pass the password on stdin?
321 openssh_process = subprocess.Popen (["ssh-keygen",
325 stdin = subprocess.PIPE,
326 stdout = subprocess.PIPE,
327 stderr = subprocess.STDOUT,
330 # TODO: argh. there has to be a better way
331 openssh_process.stdin.close ()
332 while openssh_process.poll () is None:
333 sys.stdout.write (openssh_process.stdout.readline ())
336 line = openssh_process.stdout.readline ()
339 sys.stdout.write (line.decode ("UTF-8"))
341 if openssh_process.returncode != 0:
342 raise Exception ("Couldn't create OpenSSH key.")
345 # http://www.blog.pythonlibrary.org/2014/07/14/tkinter-redirecting-stdout-stderr/
346 # http://www.virtualroadside.com/blog/index.php/2012/11/10/glib-idle_add-for-tkinter-in-python/
347 class RedirectText (object):
348 def __init__ (self, root, widget):
352 self.queue = Queue ()
354 def write (self, string):
355 self.widget.insert (END, string)
357 def enqueue (self, value):
358 self.queue.put (value)
359 self.root.event_generate ("<<Idle>>", when = "tail")
362 class CryptoInstallProgress (Toplevel):
363 def __init__ (self, parent):
364 Toplevel.__init__ (self, parent)
368 self.create_widgets ()
370 def create_widgets (self):
371 self.balloon = Balloon (self, initwait = 250)
373 self.text = ScrolledText (self)
374 self.text.pack (fill = BOTH, expand = True)
376 self.redirect = RedirectText (self.parent, self.text)
378 self._quit = Button (self)
379 self._quit["text"] = "Quit"
380 self._quit["command"] = self.quit
381 self.balloon.bind_widget (self._quit,
382 msg = _ ("Quit the program immediately"))
386 class CryptoInstall (Tk):
387 def __init__ (self, arguments):
390 self.arguments = arguments
392 self.resizable (width = False, height = False)
393 self.title (_ ("Crypto Install Wizard"))
395 self.create_widgets ()
397 def create_widgets (self):
398 self.balloon = Balloon (self, initwait = 250)
400 self.info_frame = Frame (self)
401 self.info_frame.pack (fill = X)
403 self.user_label = Label (self.info_frame)
404 self.user_label["text"] = _ ("Username")
405 self.user_label.grid ()
407 self.user_var = StringVar ()
408 self.user_var.set (default_username ())
409 self.user_var.trace ("w", self.update_widgets)
411 self.user = Entry (self.info_frame, textvariable = self.user_var,
413 self.balloon.bind_widget (self.user, msg = dedented (_ ("""
414 Username on the local machine (e.g. 'user')
416 self.user.grid (row = 0, column = 1)
418 self.host_label = Label (self.info_frame)
419 self.host_label["text"] = _ ("Host Name")
420 self.host_label.grid ()
422 self.host_var = StringVar ()
423 self.host_var.set (default_hostname ())
424 self.host_var.trace ("w", self.update_widgets)
426 self.host = Entry (self.info_frame, textvariable = self.host_var,
428 self.balloon.bind_widget (self.host, msg = dedented (_ ("""
429 Host name of the local machine (e.g. 'mycomputer')
431 self.host.grid (row = 1, column = 1)
433 self.name_label = Label (self.info_frame)
434 self.name_label["text"] = _ ("Full Name")
435 self.name_label.grid ()
437 self.name_var = StringVar ()
438 self.name_var.set (default_name ())
439 self.name_var.trace ("w", self.update_widgets)
441 self.name = Entry (self.info_frame, textvariable = self.name_var)
442 self.balloon.bind_widget (self.name, msg = dedented (_ ("""
443 Full name as it should appear in the key description (e.g. 'John Doe')
445 self.name.grid (row = 2, column = 1)
447 self.email_label = Label (self.info_frame)
448 self.email_label["text"] = _ ("Email address")
449 self.email_label.grid ()
451 self.email_var = StringVar ()
452 self.email_var.set (default_email ())
453 self.email_var.trace ("w", self.update_widgets)
455 self.email = Entry (self.info_frame, textvariable = self.email_var)
456 self.balloon.bind_widget (self.email, msg = dedented (_ ("""
457 Email address associated with the name (e.g. '<test@example.com>')
459 self.email.grid (row = 3, column = 1)
461 self.comment_label = Label (self.info_frame)
462 self.comment_label["text"] = _ ("Comment phrase")
463 self.comment_label.grid ()
465 self.comment_var = StringVar ()
466 self.comment_var.set (default_comment ())
467 self.comment_var.trace ("w", self.update_widgets)
469 self.comment = Entry (self.info_frame, textvariable = self.comment_var)
470 self.balloon.bind_widget (self.comment, msg = dedented (_ ("""
471 Comment phrase for the GnuPG key, if any (e.g. 'key for 2014')
473 self.comment.grid (row = 4, column = 1)
475 self.options_frame = Frame (self)
476 self.options_frame.pack (fill = X)
478 self.gnupg_label = Label (self.options_frame)
479 self.gnupg_label["text"] = _ ("Generate GnuPG key")
480 self.gnupg_label.grid ()
482 self.gnupg_var = IntVar ()
483 self.gnupg_var.set (1 if self.arguments.gnupg else 0)
484 self.gnupg_var.trace ("w", self.update_widgets)
486 self.gnupg = Checkbutton (self.options_frame,
487 variable = self.gnupg_var)
488 self.gnupg.grid (row = 0, column = 1)
490 self.openssh_label = Label (self.options_frame)
491 self.openssh_label["text"] = _ ("Generate OpenSSH key")
492 self.openssh_label.grid ()
494 self.openssh_var = IntVar ()
495 self.openssh_var.set (1 if self.arguments.openssh else 0)
496 self.openssh_var.trace ("w", self.update_widgets)
498 self.openssh = Checkbutton (self.options_frame,
499 variable = self.openssh_var)
500 self.openssh.grid (row = 1, column = 1)
502 self.button_frame = Frame (self)
503 self.button_frame.pack (fill = X)
505 self._generate = Button (self.button_frame)
506 self._generate["text"] = _ ("Generate Keys")
507 self._generate["command"] = self.generate
508 self.balloon.bind_widget (
510 msg = _ ("Generate the keys as configured above"))
511 self._generate.pack (side = LEFT, fill = Y)
513 self._quit = Button (self.button_frame)
514 self._quit["text"] = "Quit"
515 self._quit["command"] = self.quit
516 self.balloon.bind_widget (self._quit,
517 msg = _ ("Quit the program immediately"))
518 self._quit.pack (side = LEFT)
520 self.update_widgets ()
522 def valid_state (self):
523 if not self.openssh_var.get () and not self.gnupg_var.get ():
526 if gnupg_exists (self.arguments) and openssh_exists (self.arguments):
529 if not valid_email (self.email_var.get ()):
532 if not valid_name (self.name_var.get ()):
535 if not valid_comment (self.comment_var.get ()):
540 def update_widgets (self, *args):
541 valid = self.valid_state ()
543 self._generate["state"] = NORMAL if valid else DISABLED
545 name = self.name_var.get ()
547 valid = valid_name (name)
548 self.name["fg"] = "black" if valid else "red"
549 self.name_label["fg"] = "black" if valid else "red"
551 email = self.email_var.get ()
553 valid = valid_email (email)
554 self.email["fg"] = "black" if valid else "red"
555 self.email_label["fg"] = "black" if valid else "red"
557 comment = self.comment_var.get ()
559 valid = valid_comment (comment)
560 self.comment["fg"] = "black" if valid else "red"
561 self.comment_label["fg"] = "black" if valid else "red"
563 exists = gnupg_exists (self.arguments)
564 self.gnupg["state"] = NORMAL if not exists else DISABLED
566 exists = openssh_exists (self.arguments)
567 self.openssh["state"] = NORMAL if not exists else DISABLED
570 if comment.strip () != "":
571 gnupg_key + " ({}) ".format (comment)
572 gnupg_key += "<{}>".format (email)
574 user = self.user_var.get ()
575 host = self.host_var.get ()
577 openssh_key = "{}@{}".format (user, host)
579 msg = dedented (_ ("""
580 Generate a GnuPG key for '{}' and configure a default setup for it
581 """)).format (gnupg_key)
583 self.balloon.bind_widget (self.gnupg, msg = msg)
584 self.balloon.bind_widget (self.gnupg_label, msg = msg)
586 msg = dedented (_ ("""
587 Generate an OpenSSH key for '{}' and configure a default setup for it
588 """)).format (openssh_key)
590 self.balloon.bind_widget (self.openssh, msg = msg)
591 self.balloon.bind_widget (self.openssh_label, msg = msg)
593 def generate_thread (self):
597 sys.stdout = self.progress.redirect
599 # TODO: capture and show stdout and stderr
600 if self.gnupg_var.get ():
601 # TODO: make get calls thread-safe
602 gnupg_setup (self.arguments,
603 self.name_var.get (),
604 self.email_var.get (),
605 self.comment_var.get ())
606 # TODO: put update into queue
607 self.update_widgets ()
609 if self.openssh_var.get ():
610 comment = "{}@{}".format (self.user_var.get (),
611 self.host_var.get ())
612 openssh_setup (self.arguments, comment)
613 # TODO: put update into queue
614 self.update_widgets ()
621 self.progress.redirect.write (self.progress.queue.get (block = False))
626 self.progress = CryptoInstallProgress (self)
628 self.bind ("<<Idle>>", self._on_idle)
630 thread = threading.Thread (target = self.generate_thread)
637 arguments = parse_arguments ()
640 # TODO: use gtk instead? would be more consistent with the pinentry style
641 # (assuming it's using gtk)
642 CryptoInstall (arguments).mainloop ()
645 gnupg_setup (arguments)
647 if arguments.openssh:
648 openssh_setup (arguments)
651 if __name__ == "__main__":