Skip to content
Snippets Groups Projects
cranspasswords.py 8.71 KiB
Newer Older
Daniel STAN's avatar
Daniel STAN committed
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""cranspasswords: gestion des mots de passe du Cr@ns"""

import sys
import subprocess
import json
Daniel STAN's avatar
Daniel STAN committed
import tempfile
import os
import atexit
Daniel STAN's avatar
Daniel STAN committed

######
## GPG Definitions

GPG = '/usr/bin/gpg'
GPG_ARGS = {
    'decrypt': ['-d'],
    'encrypt': ['--armor', '-es'],
    'fingerprint': ['--fingerprint'],
    'receive-keys': ['--recv-keys'],
    }

Daniel STAN's avatar
Daniel STAN committed
DEBUG=False
CLIPBOARD=False # Par défaut, place-t-on le mdp dans le presse-papier ?

Daniel STAN's avatar
Daniel STAN committed
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)
Daniel STAN's avatar
Daniel STAN committed
    if DEBUG:
        stderr=sys.stderr
    else:
        stderr=subprocess.PIPE
        full_command.extend(['--debug-level=1'])
    #print full_command
Daniel STAN's avatar
Daniel STAN committed
    proc = subprocess.Popen(full_command,
                            stdin = subprocess.PIPE,
                            stdout = subprocess.PIPE,
Daniel STAN's avatar
Daniel STAN committed
                            stderr = stderr,
Daniel STAN's avatar
Daniel STAN committed
                            close_fds = True)
Daniel STAN's avatar
Daniel STAN committed
    if not DEBUG:
        proc.stderr.close()
Daniel STAN's avatar
Daniel STAN committed
    return proc.stdin, proc.stdout

######
## Remote commands

SSH = '/usr/bin/ssh'
SSH_HOST = 'localhost'
Daniel STAN's avatar
Daniel STAN committed
REMOTE_COMMAND = ['/home/dstan/crans/cranspasswords/cranspasswords-server.py']
Daniel STAN's avatar
Daniel STAN committed

def ssh(command, arg = None):
    """Lance ssh avec les arguments donnés. Renvoie son entrée
    standard et sa sortie standard."""
    full_command = [SSH, SSH_HOST]
    full_command.extend(REMOTE_COMMAND)
    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)

######
## 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])
Daniel STAN's avatar
Daniel STAN committed
            if DEBUG:   print "Checking %s" % mail
            if str("<%s>" % mail.lower()) not in stdout.read().lower():
                if DEBUG:   print "-->Fail on %s" % mail
Daniel STAN's avatar
Daniel STAN committed
                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(email)

    stdin, stdout = gpg("encrypt", email_recipients)
    stdin.write(contents)
    stdin.close()
Daniel STAN's avatar
Daniel STAN committed
    out = stdout.read()
    if out == '':
        if DEBUG: print "Échec de chiffrement"
        return None
    else:
        return out
Daniel STAN's avatar
Daniel STAN committed

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)
Daniel STAN's avatar
Daniel STAN committed
    if enc_pwd <> None:
        return put_file(name, roles, enc_pwd)
    else:
        return False
Daniel STAN's avatar
Daniel STAN committed

def get_password(name):
    """Récupère le mot de passe donné par name"""
    remotefile = get_file(name)
    return decrypt(remotefile['contents'])

Daniel STAN's avatar
Daniel STAN committed
## Interface
def usage():
    print """Cranspasswords 2 Usage:
 cranspasswords [options] [<filename>]
 cranspasswords <filename>      Télécharge le fichier
 cranspasswords                 Mode interactif

Options:
 --view                     Télécharge le fichier
# --upload                   Upload un nouveau fichier depuis stdin
 --edit                     Lance $EDITOR sur le fichier
# --roles=<role1>,<role2>…   Définit les rôles
# --roles+=<role>            Ajoute un rôle
# --roles-=<role>            Supprime un rôle
# --edit-roles               Lance $EDITOR sur les rôles
# --rm                       Supprime le fichier
 --update-keys              Mets à jour les clés
 --check-keys               Vérifie les clés
 -l, --list                 Liste les fichiers disponibles
 --list-roles               Liste des rôles disponibles
 -c, --clipboard            mot de passe en presse papier"""

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():
    print """Liste des fichiers disponibles""" 
    for fname in all_files():
        print " * " + fname

def show_roles():
    print """Liste des roles disponibles""" 
    for role in all_roles().keys():
        if role.endswith('-w'): continue
        print " * " + role 

def clipboard(texte):
    proc =subprocess.Popen(['xclip','-selection','clipboard'],\
        stdin=subprocess.PIPE,stdout=sys.stdout,stderr=sys.stderr)
    proc.stdin.write(texte)
    proc.stdin.close()
    print "[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
    print "Fichier %s:" % fname
    (sin,sout) = gpg('decrypt')
    sin.write(value['contents'])
    sin.close()
    texte = sout.read()
    if CLIPBOARD:    # Ça ne va pas plaire à tout le monde
        lines = texte.split('\n')
        if len(lines) == 2:
            clipboard(lines[0])
        else:
            for line in lines:
                if line.startswith('pass:'):
                    clipboard(line[5:].strip(' \t\r\n'))
                else:
                    print line
    else:
        print texte
    print "-----"
    print "Visible par: %s" % ','.join(value['roles'])
    # Todo: some clipboard facility
        
def edit_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 = editor(texte)
    if ntexte == None:
        print "Pas de modifications effectuées"
    else:
        if put_password(fname,value['roles'],ntexte):
            print "Modifications enregistrées"
        else:
            print "Erreur lors de l'enregistrement (avez-vous les droits suffisants ?)"
        

if __name__ == "__main__":
    argv = sys.argv[1:]
    if '-c' in argv or '--clipboard' in argv:
        CLIPBOARD=True
    action = show_file
    if '--edit' in argv:
        action = edit_file
    if '-v' in argv:    #Verbose !
        DEBUG = True
    for arg in argv:
        if arg in ['--list','-l']:
            show_files()
        elif not arg.startswith('-'):
            action(arg)
        elif arg == '--check-keys':
            print check_keys() and "Base de clés ok" or "Erreurs dans la base"
        elif arg == '--update-keys':
            print update_keys()
        elif arg == '--list-roles':
            show_roles()
        elif arg in ['-c','--clipboard','--view','--edit','-v']:
            pass
        else:
            usage()
            break
Daniel STAN's avatar
Daniel STAN committed