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 *
13 def input_string (prompt=""):
14 return raw_input (prompt)
15 elif sys.version_info[0] > 2:
17 from tkinter.messagebox import *
18 from tkinter.tix import *
20 def input_string (prompt=""):
23 raise Exception ("Unsupported Python version {}".format (sys.version_info))
27 return textwrap.dedent (text).strip ()
31 return textwrap.dedent (text).lstrip ()
35 return textwrap.fill (dedented (text), width = 72)
39 return textwrap.fill (ldedented (text), width = 72)
42 def read_input_string (prompt="", default=""):
44 readline.set_startup_hook (lambda: readline.insert_text (default))
47 return input_string(prompt)
49 readline.set_startup_hook()
52 def parse_arguments ():
53 parser = argparse.ArgumentParser ()
58 version = "crypto-install.py version GIT-TAG (GIT-COMMIT/GIT-BRANCH)",
59 help = "Display version.")
63 action = "store_false",
64 help = "Disable GUI, use text mode.")
65 gnupg_group = parser.add_argument_group ("GnuPG",
66 "Options related to the GnuPG setup.")
67 gnupg_group.add_argument (
70 action = "store_false",
71 help = "Disable GnuPG setup.")
72 gnupg_group.add_argument (
77 help = "Default directory for GnuPG files.")
78 openssh_group = parser.add_argument_group ("OpenSSH",
79 "Options related to the OpenSSH setup.")
80 openssh_group.add_argument (
83 action = "store_false",
84 help = "Disable OpenSSH setup.")
85 openssh_group.add_argument (
87 dest = "openssh_home",
90 help = "Default directory for OpenSSH files.")
91 return parser.parse_args ()
94 def ensure_directories (path, mode = 0o777):
96 os.makedirs (path, mode)
97 except OSError as exception:
98 if exception.errno != errno.EEXIST:
103 return os.getenv ("FULLNAME")
106 def default_email ():
107 return os.getenv ("EMAIL")
110 def default_comment ():
114 def default_hostname ():
115 return subprocess.check_output ("hostname").strip ()
118 def default_username ():
119 return os.getenv ("USER")
122 def valid_email (value):
123 return re.match (".+@.+", value)
126 def valid_name (value):
127 return value.strip () != ""
130 def valid_comment (value):
134 def gnupg_exists (arguments):
135 gnupg_home = os.path.expanduser (arguments.gnupg_home)
136 gnupg_secring = os.path.join (gnupg_home, "secring.gpg")
138 return os.path.exists (gnupg_secring)
141 def openssh_exists (arguments):
142 openssh_home = os.path.expanduser (arguments.openssh_home)
143 openssh_config = os.path.join (openssh_home, "config")
144 openssh_key = os.path.join (openssh_home, "id_rsa")
146 return os.path.exists (openssh_config) and os.path.exists (openssh_key)
149 # TODO: verify phrase at least once
150 # TODO: use better labels
151 def input_passphrase (arguments):
152 batch_passphrase = ldedented ("""
156 """).format (subprocess.check_output ("tty").strip (),
159 batch_env = dict (os.environ)
161 batch_passphrase += ldedented ("""
164 """).format (os.getenv ("XAUTHORITY"),
165 os.getenv ("DISPLAY"))
167 del batch_env["DISPLAY"]
169 batch_passphrase += "GET_PASSPHRASE --data --check --qualitybar X X Passphrase X\n"
171 passphrase_process = subprocess.Popen (["gpg-agent", "--server"],
172 stdin = subprocess.PIPE,
173 stdout = subprocess.PIPE,
174 stderr = subprocess.PIPE,
176 (stdout, stderr) = passphrase_process.communicate (batch_passphrase)
178 if passphrase_process.returncode != 0:
179 raise Exception ("Couldn't read passphrase.")
181 for line in stdout.splitlines ():
182 if line.startswith ("D "):
188 def gnupg_setup (arguments, name = None, email = None, comment = None):
189 gnupg_home = os.path.expanduser (arguments.gnupg_home)
190 gnupg_secring = os.path.join (gnupg_home, "secring.gpg")
192 if gnupg_exists (arguments):
193 print ("GnuPG secret keyring already exists at '{}'."
194 .format (gnupg_secring))
197 if not arguments.gui:
199 No default GnuPG key available. Please enter your information to
200 create a new key."""))
202 name = read_input_string ("What is your name (e.g. 'John Doe')? ",
205 email = read_input_string (dedented ("""
206 What is your email address (e.g. 'test@example.com')? """),
209 comment = read_input_string (dedented ("""
210 What is your comment phrase, if any (e.g. 'key for 2014')? """),
213 if not os.path.exists (gnupg_home):
214 print ("Creating GnuPG directory at '{}'.".format (gnupg_home))
215 ensure_directories (gnupg_home, 0o700)
217 with tempfile.NamedTemporaryFile () as tmp:
218 batch_key = ldedented ("""
227 """).format (name, email)
230 batch_key += "Name-Comment: {}\n".format (comment)
232 tmp.write (batch_key)
235 batch_env = dict (os.environ)
236 if not arguments.gui:
237 del batch_env["DISPLAY"]
239 gnupg_process = subprocess.Popen (["gpg2",
240 "--homedir", gnupg_home,
241 "--batch", "--gen-key", tmp.name],
243 gnupg_process.wait ()
245 if gnupg_process.returncode != 0:
246 raise Exception ("Couldn't create GnuPG key.")
249 def openssh_setup (arguments, comment = None):
250 openssh_home = os.path.expanduser (arguments.openssh_home)
251 openssh_config = os.path.join (openssh_home, "config")
253 if not os.path.exists (openssh_config):
254 print ("Creating OpenSSH directory at '{}'.".format (openssh_home))
255 ensure_directories (openssh_home, 0o700)
257 print ("Creating OpenSSH configuration at '{}'.".format (openssh_config))
258 with open (openssh_config, "w") as config:
259 config.write (ldedented ("""
264 openssh_key = os.path.join (openssh_home, "id_rsa")
266 if os.path.exists (openssh_key):
267 print ("OpenSSH key already exists at '{}'.".format (openssh_key))
270 print (filled ("No OpenSSH key available. Generating new key."))
272 if not arguments.gui:
273 comment = "{}@{}".format (default_username (), default_hostname ())
274 comment = read_input_string (ldedented ("""
275 What is your comment phrase (e.g. 'user@mycomputer')? """), comment)
277 passphrase = input_passphrase (arguments)
279 # TODO: is it somehow possible to pass the password on stdin?
280 openssh_process = subprocess.Popen (["ssh-keygen",
284 openssh_process.wait ()
286 if openssh_process.returncode != 0:
287 raise Exception ("Couldn't create OpenSSH key.")
290 class CryptoInstall (Tk):
291 def __init__ (self, arguments):
294 self.arguments = arguments
296 self.resizable (width = False, height = False)
297 self.title ("Crypto Install Wizard")
299 self.create_widgets ()
302 def create_widgets (self):
303 self.balloon = Balloon (self, initwait = 250)
306 self.info_frame = Frame (self)
307 self.info_frame.pack (fill = X)
310 self.user_label = Label (self.info_frame)
311 self.user_label["text"] = "Username"
312 self.user_label.grid ()
314 self.user_var = StringVar ()
315 self.user_var.set (default_username ())
316 self.user_var.trace ("w", self.update_widgets)
318 self.user = Entry (self.info_frame, textvariable = self.user_var,
320 self.balloon.bind_widget (self.user, msg = dedented ("""
321 Username on the local machine (e.g. 'user')
323 self.user.grid (row = 0, column = 1)
326 self.host_label = Label (self.info_frame)
327 self.host_label["text"] = "Host Name"
328 self.host_label.grid ()
330 self.host_var = StringVar ()
331 self.host_var.set (default_hostname ())
332 self.host_var.trace ("w", self.update_widgets)
334 self.host = Entry (self.info_frame, textvariable = self.host_var,
336 self.balloon.bind_widget (self.host, msg = dedented ("""
337 Host name of the local machine (e.g. 'mycomputer')
339 self.host.grid (row = 1, column = 1)
342 self.name_label = Label (self.info_frame)
343 self.name_label["text"] = "Full Name"
344 self.name_label.grid ()
346 self.name_var = StringVar ()
347 self.name_var.set (default_name ())
348 self.name_var.trace ("w", self.update_widgets)
350 self.name = Entry (self.info_frame, textvariable = self.name_var)
351 self.balloon.bind_widget (self.name, msg = dedented ("""
352 Full name as it should appear in the key description (e.g. 'John Doe')
354 self.name.grid (row = 2, column = 1)
357 self.email_label = Label (self.info_frame)
358 self.email_label["text"] = "Email address"
359 self.email_label.grid ()
361 self.email_var = StringVar ()
362 self.email_var.set (default_email ())
363 self.email_var.trace ("w", self.update_widgets)
365 self.email = Entry (self.info_frame, textvariable = self.email_var)
366 self.balloon.bind_widget (self.email, msg = dedented ("""
367 Email address associated with the name (e.g. '<test@example.com>')
369 self.email.grid (row = 3, column = 1)
372 self.comment_label = Label (self.info_frame)
373 self.comment_label["text"] = "Comment phrase"
374 self.comment_label.grid ()
376 self.comment_var = StringVar ()
377 self.comment_var.set (default_comment ())
378 self.comment_var.trace ("w", self.update_widgets)
380 self.comment = Entry (self.info_frame, textvariable = self.comment_var)
381 self.balloon.bind_widget (self.comment, msg = dedented ("""
382 Comment phrase for the GnuPG key, if any (e.g. 'key for 2014')
384 self.comment.grid (row = 4, column = 1)
387 self.options_frame = Frame (self)
388 self.options_frame.pack (fill = X)
390 self.gnupg_label = Label (self.options_frame)
391 self.gnupg_label["text"] = "Generate GnuPG key"
392 self.gnupg_label.grid ()
394 self.gnupg_var = IntVar ()
395 self.gnupg_var.set (1 if self.arguments.gnupg else 0)
396 self.gnupg_var.trace ("w", self.update_widgets)
398 self.gnupg = Checkbutton (self.options_frame, variable = self.gnupg_var)
399 self.gnupg.grid (row = 0, column = 1)
401 self.openssh_label = Label (self.options_frame)
402 self.openssh_label["text"] = "Generate OpenSSH key"
403 self.openssh_label.grid ()
405 self.openssh_var = IntVar ()
406 self.openssh_var.set (1 if self.arguments.openssh else 0)
407 self.openssh_var.trace ("w", self.update_widgets)
409 self.openssh = Checkbutton (self.options_frame,
410 variable = self.openssh_var)
411 self.openssh.grid (row = 1, column = 1)
414 self.button_frame = Frame (self)
415 self.button_frame.pack (fill = X)
417 self._generate = Button (self.button_frame)
418 self._generate["text"] = "Generate Keys"
419 self._generate["command"] = self.generate
420 self.balloon.bind_widget (self._generate,
421 msg = "Generate the keys as configured above")
422 self._generate.pack (side = LEFT, fill = Y)
424 self._quit = Button (self.button_frame)
425 self._quit["text"] = "Quit"
426 self._quit["command"] = self.quit
427 self.balloon.bind_widget (self._quit,
428 msg = "Quit the program immediately")
429 self._quit.pack (side = LEFT)
431 self.update_widgets ()
434 def valid_state (self):
435 if not self.openssh_var.get () and not self.gnupg_var.get ():
438 if gnupg_exists (self.arguments) and openssh_exists (self.arguments):
441 if not valid_email (self.email_var.get ()):
444 if not valid_name (self.name_var.get ()):
447 if not valid_comment (self.comment_var.get ()):
453 def update_widgets (self, *args):
454 valid = self.valid_state ()
456 self._generate["state"] = NORMAL if valid else DISABLED
458 name = self.name_var.get ()
460 valid = valid_name (name)
461 self.name["fg"] = "black" if valid else "red"
462 self.name_label["fg"] = "black" if valid else "red"
464 email = self.email_var.get ()
466 valid = valid_email (email)
467 self.email["fg"] = "black" if valid else "red"
468 self.email_label["fg"] = "black" if valid else "red"
470 comment = self.comment_var.get ()
472 valid = valid_comment (comment)
473 self.comment["fg"] = "black" if valid else "red"
474 self.comment_label["fg"] = "black" if valid else "red"
476 exists = gnupg_exists (self.arguments)
477 self.gnupg["state"] = NORMAL if not exists else DISABLED
479 exists = openssh_exists (self.arguments)
480 self.openssh["state"] = NORMAL if not exists else DISABLED
483 if comment.strip () != "":
484 gnupg_key + " ({}) ".format (comment)
485 gnupg_key += "<{}>".format (email)
487 user = self.user_var.get ()
488 host = self.host_var.get ()
490 openssh_key = "{}@{}".format (user, host)
493 Generate a GnuPG key for '{}' and configure a default setup for it
494 """).format (gnupg_key)
496 self.balloon.bind_widget (self.gnupg, msg = msg)
497 self.balloon.bind_widget (self.gnupg_label, msg = msg)
500 Generate an OpenSSH key for '{}' and configure a default setup for it
501 """).format (openssh_key)
503 self.balloon.bind_widget (self.openssh, msg = msg)
504 self.balloon.bind_widget (self.openssh_label, msg = msg)
508 # TODO: capture and show stdout and stderr
509 if self.gnupg_var.get ():
510 gnupg_setup (self.arguments,
511 self.name_var.get (),
512 self.email_var.get (),
513 self.comment_var.get ())
515 if self.openssh_var.get ():
516 comment = "{}@{}".format (self.user_var.get (),
517 self.host_var.get ())
518 openssh_setup (self.arguments, comment)
520 # TODO: show summary before exiting
524 # TODO: use gtk instead? would be more consistent with the pinentry style
525 # (assuming it's using gtk)
527 app = CryptoInstall (arguments)
532 arguments = parse_arguments ()
538 gnupg_setup (arguments)
540 if arguments.openssh:
541 openssh_setup (arguments)
544 if __name__ == "__main__":