#!/usr/bin/env python # -*- encoding: utf-8 -*- """cranspasswords: gestion des mots de passe du Cr@ns""" import sys import subprocess import json import tempfile import os import atexit import argparse import re try: import clientconfig as config except ImportError: print "Read the README" sys.exit(1) ## Password pattern in files: PASS = re.compile('[\t ]*pass(?:word)?[\t ]*:[\t ]*(.*)\r?\n?$', \ flags=re.IGNORECASE) ###### ## GPG Definitions GPG = '/usr/bin/gpg' GPG_ARGS = { 'decrypt': ['-d'], 'encrypt': ['--armor', '-es'], 'fingerprint': ['--fingerprint'], 'receive-keys': ['--recv-keys'], } DEBUG = False VERB = False # Par défaut, place-t-on le mdp dans le presse-papier ? CLIPBOARD = bool(os.getenv('DISPLAY')) and os.path.exists('/usr/bin/xclip') FORCED = False #Mode interactif qui demande confirmation NROLES = None # Droits à définir sur le fichier en édition SERVER = None def gpg(command, args = None): """Lance gpg pour la commande donnée avec les arguments donnés. Renvoie son entrée standard et sa sortie standard.""" full_command = [GPG] full_command.extend(GPG_ARGS[command]) if args: full_command.extend(args) if VERB: stderr=sys.stderr else: stderr=subprocess.PIPE full_command.extend(['--debug-level=1']) #print full_command proc = subprocess.Popen(full_command, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = stderr, close_fds = True) if not VERB: proc.stderr.close() return proc.stdin, proc.stdout ###### ## Remote commands def ssh(command, arg = None): """Lance ssh avec les arguments donnés. Renvoie son entrée standard et sa sortie standard.""" full_command = list(SERVER['server_cmd']) full_command.append(command) if arg: full_command.append(arg) proc = subprocess.Popen(full_command, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = sys.stderr, close_fds = True) return proc.stdin, proc.stdout def remote_command(command, arg = None, stdin_contents = None): """Exécute la commande distante, et retourne la sortie de cette commande""" sshin, sshout = ssh(command, arg) if stdin_contents: sshin.write(json.dumps(stdin_contents)) sshin.close() return json.loads(sshout.read()) def all_keys(): """Récupère les clés du serveur distant""" return remote_command("listkeys") def all_roles(): """Récupère les roles du serveur distant""" return remote_command("listroles") def all_files(): """Récupère les fichiers du serveur distant""" return remote_command("listfiles") def get_file(filename): """Récupère le contenu du fichier distant""" return remote_command("getfile", filename) def put_file(filename, roles, contents): """Dépose le fichier sur le serveur distant""" return remote_command("putfile", filename, {'roles': roles, 'contents': contents}) def rm_file(filename): """Supprime le fichier sur le serveur distant""" return remote_command("rmfile", filename) def get_my_roles(): """Retoure la liste des rôles perso""" allr = all_roles() return filter(lambda role: SERVER['user'] in allr[role],allr.keys()) ###### ## Local commands def update_keys(): """Met à jour les clés existantes""" keys = all_keys() _, stdout = gpg("receive-keys", [key for _, key in keys.values() if key]) return stdout.read() def check_keys(): """Vérifie les clés existantes""" keys = all_keys() for mail, key in keys.values(): if key: _, stdout = gpg("fingerprint", [key]) if VERB: print "Checking %s" % mail if str("<%s>" % mail.lower()) not in stdout.read().lower(): if VERB: print "-->Fail on %s" % mail break else: return True return False def encrypt(roles, contents): """Chiffre le contenu pour les roles donnés""" recipients = set() allroles = all_roles() allkeys = all_keys() email_recipients = [] for role in roles: for recipient in allroles[role]: recipients.add(recipient) for recipient in recipients: email, key = allkeys[recipient] if key: email_recipients.append("-r") email_recipients.append(key) stdin, stdout = gpg("encrypt", email_recipients) stdin.write(contents) stdin.close() out = stdout.read() if out == '': if VERB: print "Échec de chiffrement" return None else: return out def decrypt(contents): """Déchiffre le contenu""" stdin, stdout = gpg("decrypt") stdin.write(contents) stdin.close() return stdout.read() def put_password(name, roles, contents): """Dépose le mot de passe après l'avoir chiffré pour les destinataires donnés""" enc_pwd = encrypt(roles, contents) if NROLES != None: roles = NROLES if VERB: print "Pas de nouveaux rôles" if enc_pwd <> None: return put_file(name, roles, enc_pwd) else: return False def get_password(name): """Récupère le mot de passe donné par name""" remotefile = get_file(name) return decrypt(remotefile['contents']) ## Interface def editor(texte): """ Lance $EDITOR sur texte""" f = tempfile.NamedTemporaryFile() atexit.register(f.close) f.write(texte) f.flush() proc = subprocess.Popen(os.getenv('EDITOR') + ' ' + f.name,shell=True) os.waitpid(proc.pid,0) f.seek(0) ntexte = f.read() f.close() return texte <> ntexte and ntexte or None def show_files(): proc = subprocess.Popen("cat",stdin=subprocess.PIPE,shell=True) out = proc.stdin out.write("""Liste des fichiers disponibles\n""" ) my_roles = get_my_roles() files = all_files() keys = files.keys() keys.sort() for fname in keys: froles = files[fname] access = set(my_roles).intersection(froles) != set([]) out.write(" %s %s (%s)\n" % ((access and '+' or '-'),fname,", ".join(froles))) out.write("""--Mes roles: %s\n""" % \ ", ".join(my_roles)) out.close() os.waitpid(proc.pid,0) def show_roles(): print """Liste des roles disponibles""" for role in all_roles().keys(): if role.endswith('-w'): continue print " * " + role old_clipboard = None def saveclipboard(restore=False): global old_clipboard if restore and old_clipboard == None: return act = '-in' if restore else '-out' proc =subprocess.Popen(['xclip',act,'-selection','clipboard'],\ stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=sys.stderr) if not restore: old_clipboard = proc.stdout.read() else: raw_input("Appuyez sur Entrée pour récupérer le contenu précédent du presse papier.") proc.stdin.write(old_clipboard) proc.stdin.close() proc.stdout.close() def clipboard(texte): saveclipboard() proc =subprocess.Popen(['xclip','-selection','clipboard'],\ stdin=subprocess.PIPE,stdout=sys.stdout,stderr=sys.stderr) proc.stdin.write(texte) proc.stdin.close() return "[Le mot de passe a été mis dans le presse papier]" def show_file(fname): value = get_file(fname) if value == False: print "Fichier introuvable" return (sin,sout) = gpg('decrypt') sin.write(value['contents']) sin.close() texte = sout.read() ntexte = "" hidden = False # Est-ce que le mot de passe a été caché ? lines = texte.split('\n') for line in lines: catchPass = PASS.match(line) if catchPass != None and CLIPBOARD: hidden=True line = clipboard(catchPass.group(1)) ntexte += line + '\n' showbin = "cat" if hidden else "less" proc = subprocess.Popen(showbin, stdin=subprocess.PIPE, shell=True) out = proc.stdin out.write("Fichier %s:\n\n" % fname) out.write(ntexte) out.write("-----\n") out.write("Visible par: %s\n" % ','.join(value['roles'])) out.close() os.waitpid(proc.pid, 0) def edit_file(fname): value = get_file(fname) nfile = False if value == False: nfile = True print "Fichier introuvable" if not confirm("Créer fichier ?"): return texte = "" roles = get_my_roles() # Par défaut les roles d'un fichier sont ceux en écriture de son # créateur roles = [ r[:-2] for r in roles if r.endswith('-w') ] if roles == []: print "Vous ne possédez aucun rôle en écriture ! Abandon." return value = {'roles':roles} else: (sin,sout) = gpg('decrypt') sin.write(value['contents']) sin.close() texte = sout.read() ntexte = editor(texte) if ntexte == None and not nfile and NROLES == None: print "Pas de modifications effectuées" else: ntexte = texte if ntexte == None else ntexte if put_password(fname,value['roles'],ntexte): print "Modifications enregistrées" else: print "Erreur lors de l'enregistrement (avez-vous les droits suffisants ?)" def confirm(text): if FORCED: return True while True: out = raw_input(text + ' (O/N)').lower() if out == 'o': return True elif out == 'n': return False def remove_file(fname): if not confirm('Êtes-vous sûr de vouloir supprimer %s ?' % fname): return if rm_file(fname): print "Suppression achevée" else: print "Erreur de suppression (avez-vous les droits ?)" def my_check_keys(): check_keys() and "Base de clés ok" or "Erreurs dans la base" def my_update_keys(): print update_keys() def update_role(): roles = None """ Reencode les fichiers, si roles est fourni, contient une liste de rôles""" my_roles = get_my_roles() if roles == None: # On ne conserve que les rôles qui finissent par -w roles = [ r[:-2] for r in my_roles if r.endswith('-w')] if type(roles) != list: roles = [roles] for (fname,froles) in all_files().iteritems(): if set(roles).intersection(froles) == set([]): continue #if VERB: print "Reencodage de %s" % fname put_password(fname,froles,get_password(fname)) def parse_roles(strroles): if strroles == None: return None roles = all_roles() my_roles = filter(lambda r: SERVER['user'] in roles[r],roles.keys()) my_roles_w = [ r[:-2] for r in my_roles if r.endswith('-w') ] ret = set() writable = False for role in strroles.split(','): if role not in roles.keys(): print("Le rôle %s n'existe pas !" % role) return False if role.endswith('-w'): print("Le rôle %s ne devrait pas être utilisé ! (utilisez %s)" % (role,role[:-2])) return False writable = writable or role in my_roles_w ret.add(role) if not FORCED and not writable: if not confirm("""Vous vous apprêtez à perdre vos droits d'écritures\ (role ne contient pas %s) sur ce fichier, continuer ?""" % \ ", ".join(my_roles_w)): return False return list(ret) if __name__ == "__main__": parser = argparse.ArgumentParser(description="trousseau crans") parser.add_argument('--server',default='default', help='Utilisation d\'un serveur alternatif (test, etc)') parser.add_argument('-v','--verbose',action='store_true',default=False, help="Mode verbeux") parser.add_argument('-c','--clipboard',action='store_true',default=None, help="Stocker le mot de passe dans le presse papier") parser.add_argument('--noclipboard',action='store_false',default=None, dest='clipboard', help="Ne PAS stocker le mot de passe dans le presse papier") parser.add_argument('-f','--force',action='store_true',default=False, help="Forcer l'action") # Actions possibles action_grp = parser.add_mutually_exclusive_group(required=False) action_grp.add_argument('--edit',action='store_const',dest='action', default=show_file,const=edit_file, help="Editer (ou créer)") action_grp.add_argument('--view',action='store_const',dest='action', default=show_file,const=show_file, help="Voir") action_grp.add_argument('--remove',action='store_const',dest='action', default=show_file,const=remove_file, help="Effacer") action_grp.add_argument('-l','--list',action='store_const',dest='action', default=show_file,const=show_files, help="Lister les fichiers") action_grp.add_argument('--check-keys',action='store_const',dest='action', default=show_file,const=my_check_keys, help="Vérifier les clés") action_grp.add_argument('--update-keys',action='store_const',dest='action', default=show_file,const=my_update_keys, help="Mettre à jour les clés") action_grp.add_argument('--list-roles',action='store_const',dest='action', default=show_file,const=show_roles, help="Lister les rôles des gens") action_grp.add_argument('--recrypt-role',action='store_const',dest='action', default=show_file,const=update_role, help="Met à jour (reencode les roles)") parser.add_argument('--roles',nargs='?',default=None, help="liste des roles à affecter au fichier") parser.add_argument('fname',nargs='?',default=None, help="Nom du fichier à afficher") parsed = parser.parse_args(sys.argv[1:]) SERVER = config.servers[parsed.server] VERB = parsed.verbose DEBUG = VERB if parsed.clipboard != None: CLIPBOARD = parsed.clipboard FORCED = parsed.force NROLES = parse_roles(parsed.roles) if NROLES != False: if parsed.action.func_code.co_argcount == 0: parsed.action() elif parsed.fname == None: print("Vous devez fournir un nom de fichier avec cette commande") parser.print_help() else: parsed.action(parsed.fname) saveclipboard(restore=True)