Skip to content
Snippets Groups Projects
client.py 40.6 KiB
Newer Older
me5na7qbjqbrp's avatar
me5na7qbjqbrp committed
#!/usr/bin/env python3

"""Gestion centralisée des mots de passe avec chiffrement GPG

Copyright (C) 2010-2020 Cr@ns <roots@crans.org>
Authors : Daniel Stan <daniel.stan@crans.org>
          Vincent Le Gallic <legallic@crans.org>
"""
Daniel STAN's avatar
Daniel STAN committed

Daniel STAN's avatar
Daniel STAN committed
import sys
import subprocess
import json
Daniel STAN's avatar
Daniel STAN committed
import tempfile
import os
Daniel STAN's avatar
Daniel STAN committed
import argparse
import re
import datetime
Daniel STAN's avatar
Daniel STAN committed
import copy
import logging
from configparser import ConfigParser
from secrets import token_urlsafe
me5na7qbjqbrp's avatar
me5na7qbjqbrp committed
# Import a SSH client
from paramiko.client import SSHClient
from paramiko.ssh_exception import SSHException
# Configuration loading
# On n'a pas encore accès à la config donc on devine le nom
bootstrap_cmd_name = os.path.split(sys.argv[0])[1]
default_config_path = os.path.expanduser("~/.config/" + bootstrap_cmd_name)
me5na7qbjqbrp's avatar
me5na7qbjqbrp committed
config_path = os.getenv(
    "CRANSPASSWORDS_CLIENT_CONFIG_DIR", default_config_path)
config = ConfigParser()
if not config.read(config_path + "/clientconfig.ini"):
    # If config could not be imported, display an error if required
    ducktape_display_error = sys.stderr.isatty() and \
me5na7qbjqbrp's avatar
me5na7qbjqbrp committed
        not any([opt in sys.argv for opt in ["-q", "--quiet"]]) and \
        __name__ == '__main__'
    if ducktape_display_error:
        # Do not use logger as it has not been initialized yet
me5na7qbjqbrp's avatar
me5na7qbjqbrp committed
        print("%s/clientconfig.ini could not be read. Please read README." %
              config_path)
        exit(1)

# Logger local
log = logging.getLogger(bootstrap_cmd_name)
Daniel STAN's avatar
Daniel STAN committed

#: Pattern utilisé pour détecter la ligne contenant le mot de passe dans les fichiers
pass_regexp = re.compile('[\t ]*pass(?:word)?[\t ]*:[\t ]*(.*)\r?\n?$',
me5na7qbjqbrp's avatar
me5na7qbjqbrp committed
                         flags=re.IGNORECASE)
me5na7qbjqbrp's avatar
me5na7qbjqbrp committed
# GPG Definitions
GPG = 'gpg'

#: Paramètres à fournir à gpg en fonction de l'action désirée
Daniel STAN's avatar
Daniel STAN committed
GPG_ARGS = {
me5na7qbjqbrp's avatar
me5na7qbjqbrp committed
    'decrypt': ['-d'],
    'encrypt': ['--armor', '-es'],
    'receive-keys': ['--recv-keys'],
    'list-keys': ['--list-keys', '--with-colons', '--fixed-list-mode',
                  '--with-fingerprint', '--with-fingerprint'],  # Ce n'est pas une erreur. Il faut 2 --with-fingerprint pour avoir les fingerprints des subkeys.
}

#: Mapping (lettre de trustlevel) -> (signification, faut-il faire confiance à la clé)
me5na7qbjqbrp's avatar
me5na7qbjqbrp committed
    "-": ("inconnue (pas de valeur assignée)", False),
    "o": ("inconnue (nouvelle clé)", False),
    "i": ("invalide (self-signature manquante ?)", False),
    "n": ("nulle (il ne faut pas faire confiance à cette clé)", False),
    "m": ("marginale (pas assez de lien de confiance vers cette clé)", False),
    "f": ("entière (clé dans le réseau de confiance)", True),
    "u": ("ultime (c'est probablement ta clé)", True),
    "r": ("révoquée", False),
    "e": ("expirée", False),
    "q": ("non définie", False),
}

def gpg(options, command, args=None):
Daniel STAN's avatar
Daniel STAN committed
    """Lance gpg pour la commande donnée avec les arguments
    donnés. Renvoie son entrée standard et sa sortie standard."""
Daniel STAN's avatar
Daniel STAN committed
    full_command = [GPG]
    full_command.extend(GPG_ARGS[command])
    if args:
        full_command.extend(args)
Daniel STAN's avatar
Daniel STAN committed
    else:
Daniel STAN's avatar
Daniel STAN committed
        full_command.extend(['--debug-level=1'])

    # Run gpg
    log.info("Running `%s`" % " ".join(full_command))
Daniel STAN's avatar
Daniel STAN committed
    proc = subprocess.Popen(full_command,
me5na7qbjqbrp's avatar
me5na7qbjqbrp committed
                            stdin=subprocess.PIPE,
                            stdout=subprocess.PIPE,
                            stderr=stderr,
                            close_fds=True)
Daniel STAN's avatar
Daniel STAN committed
        proc.stderr.close()
Daniel STAN's avatar
Daniel STAN committed
    return proc.stdin, proc.stdout

def _parse_timestamp(string, canbenone=False):
    """Interprète ``string`` comme un timestamp depuis l'Epoch."""
    if string == '' and canbenone:
        return None
    return datetime.datetime(*time.localtime(int(string))[:7])

def _parse_pub(data):
    """Interprète une ligne ``pub:``"""
    d = {
        'trustletter': data[1],
        'length': int(data[2]),
        'algorithm': int(data[3]),
        'longid': data[4],
        'signdate': _parse_timestamp(data[5]),
        'expiredate': _parse_timestamp(data[6], canbenone=True),
        'ownertrustletter': data[8],
        'capabilities': data[11],
me5na7qbjqbrp's avatar
me5na7qbjqbrp committed
    }
def _parse_uid(data):
    """Interprète une ligne ``uid:``"""
    d = {
        'trustletter': data[1],
        'signdate': _parse_timestamp(data[5], canbenone=True),
        'hash': data[7],
        'uid': data[9],
me5na7qbjqbrp's avatar
me5na7qbjqbrp committed
    }
def _parse_fpr(data):
    """Interprète une ligne ``fpr:``"""
    d = {
        'fpr': data[9],
me5na7qbjqbrp's avatar
me5na7qbjqbrp committed
    }
def _parse_sub(data):
    """Interprète une ligne ``sub:``"""
    d = {
        'trustletter': data[1],
        'length': int(data[2]),
        'algorithm': int(data[3]),
        'longid': data[4],
        'signdate': _parse_timestamp(data[5]),
        'expiredate': _parse_timestamp(data[6], canbenone=True),
        'capabilities': data[11],
me5na7qbjqbrp's avatar
me5na7qbjqbrp committed
    }
#: Functions to parse the recognized fields
GPG_PARSERS = {
    'pub': _parse_pub,
    'uid': _parse_uid,
    'fpr': _parse_fpr,
    'sub': _parse_sub,

def parse_keys(gpgout, debug=False):
    """Parse l'output d'un listing de clés gpg."""
    ring = {}
me5na7qbjqbrp's avatar
me5na7qbjqbrp committed
    # Valeur utilisée pour dire "cet objet n'a pas encore été rencontré pendant le parsing"
    init_value = "initialize"
    current_pub = init_value
    current_sub = init_value
    for line in iter(gpgout.readline, ''):
        # La doc dit que l'output est en UTF-8 «regardless of any --display-charset setting»
        try:
            line = line.decode("utf-8")
        except UnicodeDecodeError:
            try:
                line = line.decode("iso8859-1")
            except UnicodeDecodeError:
                line = line.decode("iso8859-1", "ignore")
me5na7qbjqbrp's avatar
me5na7qbjqbrp committed
                log.warning(
                    "gpg is telling shit, it is neither ISO-8859-1 nor UTF-8. Dropping!")
        line = line.split(":")
        field = line[0]
        if field in GPG_PARSERS.keys():
            log.debug("begin loop, met %s :" % (field))
            log.debug("current_pub : %r" % current_pub)
            log.debug("current_sub : %r" % current_sub)
me5na7qbjqbrp's avatar
me5na7qbjqbrp committed
            except KeyError:
                log.error("*** FAILED *** Line: %s", line)
me5na7qbjqbrp's avatar
me5na7qbjqbrp committed
            if field == "pub":
Loading
Loading full blame...