aurorepass/cranspasswords.py
Daniel STAN 9857aae8f7 Ajouts de commentaires dans l'éditeur
Il permet de tester d'un coup d'œil les destinataires d'un chiffrement.
Ceci évite donc de se faire flouter par un serveur fournissant la
mauvaise liste de destinataires.
2013-02-15 03:23:30 +01:00

503 lines
16 KiB
Python
Executable file

#!/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
class simple_memoize(object):
""" Memoization/Lazy """
def __init__(self, f):
self.f = f
self.val = None
def __call__(self):
if self.val==None:
self.val = self.f()
return self.val
######
## 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())
@simple_memoize
def all_keys():
"""Récupère les clés du serveur distant"""
return remote_command("listkeys")
@simple_memoize
def all_roles():
"""Récupère les roles du serveur distant"""
return remote_command("listroles")
@simple_memoize
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)
@simple_memoize
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 get_recipients_of_roles(roles):
"""Renvoie les destinataires d'un rôle"""
recipients = set()
allroles = all_roles()
for role in roles:
for recipient in allroles[role]:
recipients.add(recipient)
return recipients
def get_dest_of_roles(roles):
allkeys = all_keys()
return ["%s (%s -> %s)" % (rec, allkeys[rec][0], allkeys[rec][1]) for rec in \
get_recipients_of_roles(roles)]
def encrypt(roles, contents):
"""Chiffre le contenu pour les roles donnés"""
allkeys = all_keys()
recipients = get_recipients_of_roles(roles)
email_recipients = []
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, annotations=""):
""" Lance $EDITOR sur texte.
Renvoie le nouveau texte si des modifications ont été apportées, ou None
"""
# Avoid syntax hilight with ".txt". Would be nice to have some colorscheme
# for annotations ...
f = tempfile.NamedTemporaryFile(suffix='.txt')
atexit.register(f.close)
f.write(texte)
for l in annotations.split('\n'):
f.write("# %s\n" % l.encode('utf-8'))
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()
ntexte = '\n'.join(filter(lambda l: not l.startswith('#'), ntexte.split('\n')))
if texte != ntexte:
return ntexte
return 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()
value['roles'] = NROLES or value['roles']
annotations = u"Ce fichier sera chiffré pour les rôles suivants :\n%s\n\
C'est-à-dire pour les utilisateurs suivants :\n%s" % (
', '.join(value['roles']),
'\n'.join(' %s' % rec for rec in get_dest_of_roles(value['roles']))
)
ntexte = editor(texte, annotations)
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)