diff --git a/archive/gestion/chgpass.py b/archive/gestion/chgpass.py new file mode 100755 index 00000000..e60ee92f --- /dev/null +++ b/archive/gestion/chgpass.py @@ -0,0 +1,224 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Script de changement de mots de passe LDAP + +Utilisation : + * cas 1 : sans arguements par un utlisateur lambda : + changement de son propre mdp + * cas 2 : avec argument par un utilisateur ayant accès + total à la base LDAP (respbats) mais PAS ROOT : + changement du mdp de l'adhérent fourni en argument, + impossibilité de modifier le mdp d'un compte privilégié + * cas 3 : lancé par root : possibilité de modifier + tous les mots de passe LDAP + +Copyright (C) Frédéric Pauget +Licence : GPLv2 +""" + +import subprocess +import getpass, commands, os, sys, base64, syslog +from user_tests import getuser, isadm + +from affich_tools import cprint, coul +import secrets_new as secrets + +try: + ldap_password = secrets.get("ldap_password") + ldap_auth_dn = secrets.get("ldap_auth_dn") +except: + ldap_password = '' + ldap_auth_dn = '' + +uri = 'ldap://ldap.adm.crans.org' +syslog.openlog('chgpass',syslog.LOG_PID,syslog.LOG_AUTH) + +def decode64(chaine): + """ Décode une chaine de caratère utf8/64 et retourne un unicode """ + try: + return base64.decodestring(chaine).decode('utf8','ignore') + except: + return chaine.decode('utf8','ignore') + +def chgpass(dn, mdp = None) : + if mdp == None: + mdp = promptpass(dn) + + # Changement mdp + if os.system("/usr/bin/ldappasswd -H '%s' -x -D '%s' -w '%s' '%s' -s '%s' > /dev/null" % (uri, ldap_auth_dn, ldap_password, dn, mdp) ): + cprint(u'Erreur lors du changement de mot de passe', 'rouge') + syslog.syslog("LDAP password changed for dn=%s" % dn) + else : + cprint(u'Changement effectué avec succès', u'vert') + +def checkpass(mdp, dialog=False, longueur=8): + ### Test du mdp + ## 1 - Longueur + if len(mdp) < longueur : + return False, coul(u'Mot de passe trop court, il doit faire au moins %s caractères de long' % longueur, 'rouge', dialog=dialog) + + ## 2 - Empeche les mots de passe non ASCII + try: + mdp = mdp.encode('ascii') + except (UnicodeEncodeError, UnicodeDecodeError): + return False, coul(u'Les accents ou caractères bizarres ne sont pas autorisés (mais #!@*&%{}| le sont !)', + 'rouge', dialog=dialog) + + + ## 2bis - On évite une attaque de type injection de code shell + if "'" in mdp: + return False, coul(u'Les accents ou caractères bizarres ne sont pas autorisés (mais #!@*&%{}| le sont !)', + 'rouge', dialog=dialog) + + ## 3 - assez de caractères de types différents ? + chiffres = 0 + majuscules = 0 + minuscules = 0 + autres = 0 + for c in mdp[:] : + if c.isdigit() : + # Un chiffre rapporte 1.5 point avec un maximum de 5 + if chiffres < 4.5 : chiffres += 1.5 + elif c.islower() : + if minuscules < 3 : minuscules += 1 + elif c.isupper() : + if majuscules < 3 : majuscules += 1 + else : + autres += 4 + if (not majuscules and not minuscules): + return False, coul(u'Mot de passe sans majuscules et sans miniscules. Mélangez des deux ?', dialog=dialog) + if len(mdp) < 16 - minuscules - majuscules - chiffres - autres: + return False, coul(u'Mot de passe trop simple. Ajoutez des chiffres ou des caractères parmis #!@*&%{}| ou mélangez des majuscules/minuscules ?', 'rouge', dialog=dialog) + + ## 4 - Cracklib + p = subprocess.Popen(['/usr/sbin/cracklib-check'], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) + (test, err) = p.communicate(input=mdp) + if test.split(':')[-1].lower().strip() != 'ok' : + commentaire = { + ' it does not contain enough DIFFERENT characters': u'Il y a trop de caractères identiques.' , + ' it is based on a dictionary word': u'Le mot de passe est basé sur un mot du dictionnaire' , + ' it is too simplistic/systematic': u'Le mot de passe est trop simple/répétitif' + }.get(test.split(':')[-1],test.split(':')[-1]) + return False, coul(commentaire.strip(), 'rouge', dialog=dialog) + + return True, "" + +def promptpass(dn): + cprint(u"""Le nouveau mot de passe doit comporter au minimum 6 caractères. +Il ne doit pas être basé sur un mot du dictionnaire.""", 'jaune') + print u"Il est conseillé d'utiliser une combinaison de minuscules, majuscules,\nde chiffres et d'au moins un caractère spécial." + print u"Le mot de passe tapé ne sera pas écrit à l'écran." + print u"Taper Ctrl-D pour abandonner" + + try : + while 1 : + mdp = getpass.getpass('Nouveau mot de passe : ') + + (good, txt) = checkpass(mdp) + if not good: + print txt + continue + + ### On redemande le mot de passe + mdp1 = getpass.getpass('Retaper mot de passe : ') + if mdp != mdp1 : + cprint(u'Les deux mots de passe entrés sont différents, réesayer', 'rouge') + continue + break + + except KeyboardInterrupt : + cprint(u'\nAbandon', 'rouge') + sys.exit(1) + except EOFError : + # Un Ctrl-D + cprint(u'\nAbandon', 'rouge') + sys.exit(1) + + return mdp + +if __name__ == '__main__' : + sys.stdout.write('\r \r') # Pour esthétique lors de l'utilisation par sudo + if len(sys.argv) == 1 : + # Changement de son mot de passe + login = getuser() + self_mode = True + + elif '-h' in sys.argv or '--help' in sys.argv or len(sys.argv) != 2 : + print u"%s " % sys.argv[0].split('/')[-1].split('.')[0] + print u"Changement du mot de passe du compte choisi." + sys.exit(255) + else : + # Changement du mot de passe par un câbleur ou une nounou + login = sys.argv[1] + self_mode = False + for c in login[:] : + if not c.isalnum() and not c=='-' : + cprint(u'Login incorrect', 'rouge') + sys.exit(1) + + if getuser() == login : + cprint(u'Utiliser passwd pour changer son propre mot de passe', 'rouge') + sys.exit(2) + + if self_mode : + s = commands.getoutput('sudo -u respbats ldap_whoami') + else : + s = commands.getoutput("/usr/bin/ldapsearch -D cn=readonly,dc=crans,dc=org -y/etc/ldap/readonly -x -LLL '(&(objectClass=posixAccount)(uid=%s))' dn nom prenom droits | grep -v MultiMachines" % login).strip() + if not s : + cprint(u'Login non trouvé dans la base LDAP', 'rouge') + sys.exit(3) + + # Ca a l'air bon + if s.find('\n\n') != -1 : + # Plusieurs trouvé : pas normal + cprint(u'Erreur lors de la recherche du login : plusieurs occurences !', 'rouge') + sys.exit(4) + + s = s.strip().split('\n') + dn = s[0].split()[1] + try : + if len(s) == 2 : + cprint(u"Changement du mot de passe du club %s "%decode64(' '.join(s[1].split()[1:])), 'vert') + else : + cprint(u"Changement du mot de passe de %s %s " % ( s[2].split()[1], s[1].split()[1] ), 'vert') + except : + cprint(u'Erreur lors de la recherche du login', 'rouge') + sys.exit(5) + + if self_mode : + # Il faut vérifier l'ancien mot de passe + ldap_auth_dn = dn + ldap_password = getpass.getpass('Mot de passe actuel : ') + s = commands.getoutput("/usr/bin/ldapwhoami -H '%s' -x -D '%s' -w '%s'" % ( uri, ldap_auth_dn, ldap_password ) ).strip() + try : + resultat = s.split('\n')[0].split(':')[1].strip() + except : + cprint(u"Erreur lors de l'authentification", 'rouge') + sys.exit(7) + if resultat != dn : + cprint({ 'Invalid credentials (49)': u'Mot de passe invalide' }.get(resultat, resultat), 'rouge') + sys.exit(8) + + elif len(s) > 3 and os.getuid()!=0 and not isadm(): + # Adhérent avec droits et on est pas root + From = 'roots@crans.org' + To = 'roots@crans.org' + mail = """From: Root <%s> +To: %s +Subject: Tentative de changement de mot de passe ! + +Tentative de changement du mot de passe de %s par %s. +""" % ( From, To , login, os.getlogin() ) + + # Envoi mail + import smtplib + conn = smtplib.SMTP('localhost') + conn.sendmail(From, To , mail ) + conn.quit() + cprint(u'Impossible de changer le mot de passe de cet adhérent : compte privilégié', 'rouge') + sys.exit(6) + + # Finalement ! + chgpass(dn) diff --git a/gestion/chgpass.py b/gestion/chgpass.py index e60ee92f..07a6a82c 100755 --- a/gestion/chgpass.py +++ b/gestion/chgpass.py @@ -1,224 +1,177 @@ -#! /usr/bin/env python +#!/bin/bash /usr/scripts/python.sh # -*- coding: utf-8 -*- """ Script de changement de mots de passe LDAP -Utilisation : - * cas 1 : sans arguements par un utlisateur lambda : - changement de son propre mdp - * cas 2 : avec argument par un utilisateur ayant accès - total à la base LDAP (respbats) mais PAS ROOT : - changement du mdp de l'adhérent fourni en argument, - impossibilité de modifier le mdp d'un compte privilégié - * cas 3 : lancé par root : possibilité de modifier - tous les mots de passe LDAP - -Copyright (C) Frédéric Pauget -Licence : GPLv2 + * Change le mot de passe de l'utilisateur donné en + argument. """ -import subprocess -import getpass, commands, os, sys, base64, syslog -from user_tests import getuser, isadm +import gestion.config as config +import config.password +import getpass +import argparse +import cracklib +import gestion.affich_tools as affich_tools +import lc_ldap.shortcuts +import lc_ldap.attributs +import sys +import os +import smtplib -from affich_tools import cprint, coul -import secrets_new as secrets +encoding = "UTF-8" +ldap = lc_ldap.shortcuts.lc_ldap_admin() +current_user = os.getenv("SUDO_USER") or os.getenv("USER") or os.getenv("LOGNAME") or getpass.getuser() -try: - ldap_password = secrets.get("ldap_password") - ldap_auth_dn = secrets.get("ldap_auth_dn") -except: - ldap_password = '' - ldap_auth_dn = '' - -uri = 'ldap://ldap.adm.crans.org' -syslog.openlog('chgpass',syslog.LOG_PID,syslog.LOG_AUTH) - -def decode64(chaine): - """ Décode une chaine de caratère utf8/64 et retourne un unicode """ +def check_password(password, no_cracklib=False): + """ + Teste le mot de passe. + * Tests custom + cracklib (sauf si no_cracklib) + """ try: - return base64.decodestring(chaine).decode('utf8','ignore') - except: - return chaine.decode('utf8','ignore') + password.decode('ascii') + except UnicodeDecodeError: + affich_tools.cprint(u'Le mot de passe ne doit contenir que des caractères ascii.', "rouge") + return False -def chgpass(dn, mdp = None) : - if mdp == None: - mdp = promptpass(dn) + # Nounou mode + if no_cracklib: + if len(password) >= config.password.root_min_len: + return True + else: + problem = False + upp = 0 + low = 0 + oth = 0 + cif = 0 - # Changement mdp - if os.system("/usr/bin/ldappasswd -H '%s' -x -D '%s' -w '%s' '%s' -s '%s' > /dev/null" % (uri, ldap_auth_dn, ldap_password, dn, mdp) ): - cprint(u'Erreur lors du changement de mot de passe', 'rouge') - syslog.syslog("LDAP password changed for dn=%s" % dn) - else : - cprint(u'Changement effectué avec succès', u'vert') + # Comptage des caractères + for char in password: + if char.isdigit(): + cif += 1 + elif char.isupper(): + upp += 1 + elif char.islower(): + low += 1 + else: + oth += 1 -def checkpass(mdp, dialog=False, longueur=8): - ### Test du mdp - ## 1 - Longueur - if len(mdp) < longueur : - return False, coul(u'Mot de passe trop court, il doit faire au moins %s caractères de long' % longueur, 'rouge', dialog=dialog) + # Recherche de manque de caractères + if cif < config.password.min_cif: + affich_tools.cprint(u'Le mot de passe doit contenir plus de chiffres.', "rouge") + problem = True + if upp < config.password.min_upp: + affich_tools.cprint(u'Le mot de passe doit contenir plus de majuscules.', "rouge") + problem = True + if low < config.password.min_low: + affich_tools.cprint(u'Le mot de passe doit contenir plus de minuscules.', "rouge") + problem = True + if oth < config.password.min_oth: + affich_tools.cprint(u'Le mot de passe doit contenir plus de caractères qui ne sont ni des chiffres, ni des majuscules, ni des minuscules.', "rouge") + problem = True - ## 2 - Empeche les mots de passe non ASCII - try: - mdp = mdp.encode('ascii') - except (UnicodeEncodeError, UnicodeDecodeError): - return False, coul(u'Les accents ou caractères bizarres ne sont pas autorisés (mais #!@*&%{}| le sont !)', - 'rouge', dialog=dialog) + # Scores sur la longueur + longueur = config.password.upp_value*upp + config.password.low_value*low + config.password.cif_value*cif + config.password.oth_value*oth + if longueur < config.password.min_len: + affich_tools.cprint(u'Le mot de passe devrait être plus long, ou plus difficile.') + problem = True + if not problem: + try: + # Le mot vient-il du dico (à améliorer, on voudrait pouvoir préciser + # la rigueur du test) ? + password = cracklib.VeryFascistCheck(password) + return True + except ValueError as e: + affich_tools.cprint(str(e).decode(), "rouge") + return False + else: + return False - ## 2bis - On évite une attaque de type injection de code shell - if "'" in mdp: - return False, coul(u'Les accents ou caractères bizarres ne sont pas autorisés (mais #!@*&%{}| le sont !)', - 'rouge', dialog=dialog) + return False - ## 3 - assez de caractères de types différents ? - chiffres = 0 - majuscules = 0 - minuscules = 0 - autres = 0 - for c in mdp[:] : - if c.isdigit() : - # Un chiffre rapporte 1.5 point avec un maximum de 5 - if chiffres < 4.5 : chiffres += 1.5 - elif c.islower() : - if minuscules < 3 : minuscules += 1 - elif c.isupper() : - if majuscules < 3 : majuscules += 1 - else : - autres += 4 - if (not majuscules and not minuscules): - return False, coul(u'Mot de passe sans majuscules et sans miniscules. Mélangez des deux ?', dialog=dialog) - if len(mdp) < 16 - minuscules - majuscules - chiffres - autres: - return False, coul(u'Mot de passe trop simple. Ajoutez des chiffres ou des caractères parmis #!@*&%{}| ou mélangez des majuscules/minuscules ?', 'rouge', dialog=dialog) +def change_password(arguments): + """ + Change le mot de passe en fonction des arguments + """ + with ldap.search(u"(uid=%s)" % (arguments.user.decode(encoding),), mode="w")[0] as user: + # Test pour vérifier que l'utilisateur courant peut modifier le mdp de user + try: + user['userPassword'] = [lc_ldap.crans_utils.hash_password("test").decode('ascii')] + user.cancel() + except EnvironmentError as e: + affich_tools.cprint(str(e).decode(encoding), "rouge") - ## 4 - Cracklib - p = subprocess.Popen(['/usr/sbin/cracklib-check'], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) - (test, err) = p.communicate(input=mdp) - if test.split(':')[-1].lower().strip() != 'ok' : - commentaire = { - ' it does not contain enough DIFFERENT characters': u'Il y a trop de caractères identiques.' , - ' it is based on a dictionary word': u'Le mot de passe est basé sur un mot du dictionnaire' , - ' it is too simplistic/systematic': u'Le mot de passe est trop simple/répétitif' - }.get(test.split(':')[-1],test.split(':')[-1]) - return False, coul(commentaire.strip(), 'rouge', dialog=dialog) + # Génération d'un mail + From = 'roots@crans.org' + To = 'roots@crans.org' + mail = """From: Root <%s> + To: %s + Subject: Tentative de changement de mot de passe ! - return True, "" + Tentative de changement du mot de passe de %s par %s. + """ % (From, To , sys.argv[1], current_user) -def promptpass(dn): - cprint(u"""Le nouveau mot de passe doit comporter au minimum 6 caractères. -Il ne doit pas être basé sur un mot du dictionnaire.""", 'jaune') - print u"Il est conseillé d'utiliser une combinaison de minuscules, majuscules,\nde chiffres et d'au moins un caractère spécial." - print u"Le mot de passe tapé ne sera pas écrit à l'écran." - print u"Taper Ctrl-D pour abandonner" + # Envoi mail + conn = smtplib.SMTP('localhost') + conn.sendmail(From, To , mail ) + conn.quit() + sys.exit(1) - try : - while 1 : - mdp = getpass.getpass('Nouveau mot de passe : ') + # On peut modifier le MDP + affich_tools.cprint("Changement du mot de passe de %s %s." % (user['prenom'][0], user['nom'][0]), "vert") - (good, txt) = checkpass(mdp) - if not good: - print txt - continue + # Règles du jeu + if arguments.verbose: + affich_tools.cprint(u"""Règles : +Longueur standard : %s, root : %s, +Minimums : chiffres : %s, minuscules : %s, majuscules : %s, autres : %s, +Scores de longueur : chiffres : %s, minuscules : %s, majuscules : %s, autres : %s, +Cracklib : %s.""" % (config.password.min_len, config.password.root_min_len, config.password.min_cif, config.password.min_low, config.password.min_upp, config.password.min_oth, config.password.cif_value, config.password.low_value, config.password.upp_value, config.password.oth_value, "Oui" * (not arguments.no_cracklib) + "Non" * (arguments.no_cracklib)), 'jaune') + else: + affich_tools.cprint(u"""Le nouveau mot de passe doit comporter au minimum %s caractères. +Il ne doit pas être basé sur un mot du dictionnaire. +Il doit contenir %s chiffre(s), %s minuscule(s), %s majuscule(s) +et au moins %s autre(s) caractère(s). +CTRL+D ou CTRL+C provoquent un abandon.""" % (config.password.min_len, config.password.min_cif, config.password.min_low, config.password.min_upp, config.password.min_oth), 'jaune') - ### On redemande le mot de passe - mdp1 = getpass.getpass('Retaper mot de passe : ') - if mdp != mdp1 : - cprint(u'Les deux mots de passe entrés sont différents, réesayer', 'rouge') - continue - break + try: + while True: + mdp = getpass.getpass("Nouveau mot de passe: ") + if check_password(mdp, arguments.no_cracklib): + mdp2 = getpass.getpass("Retaper le mot de passe: ") + if mdp != mdp2: + affich_tools.cprint(u"Les deux mots de passe diffèrent.", "rouge") + else: + break - except KeyboardInterrupt : - cprint(u'\nAbandon', 'rouge') - sys.exit(1) - except EOFError : - # Un Ctrl-D - cprint(u'\nAbandon', 'rouge') - sys.exit(1) + except KeyboardInterrupt: + affich_tools.cprint(u'\nAbandon', 'rouge') + sys.exit(1) - return mdp + except EOFError: + # Un Ctrl-D + affich_tools.cprint(u'\nAbandon', 'rouge') + sys.exit(1) -if __name__ == '__main__' : - sys.stdout.write('\r \r') # Pour esthétique lors de l'utilisation par sudo - if len(sys.argv) == 1 : - # Changement de son mot de passe - login = getuser() - self_mode = True + hashedPassword = lc_ldap.crans_utils.hash_password(mdp) + user['userPassword'] = [hashedPassword.decode('ascii')] + user.save() + affich_tools.cprint(u"Mot de passe de %s changé." % (user['uid'][0]), "vert") - elif '-h' in sys.argv or '--help' in sys.argv or len(sys.argv) != 2 : - print u"%s " % sys.argv[0].split('/')[-1].split('.')[0] - print u"Changement du mot de passe du compte choisi." - sys.exit(255) - else : - # Changement du mot de passe par un câbleur ou une nounou - login = sys.argv[1] - self_mode = False - for c in login[:] : - if not c.isalnum() and not c=='-' : - cprint(u'Login incorrect', 'rouge') - sys.exit(1) +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Recherche dans la base des adhérents", add_help=False) + parser.add_argument('-h', '--help', help="Affiche ce message et quitte.", action="store_true") + parser.add_argument('-n', '--no-cracklib', help="Permet de contourner les règles de choix du mot de passe (réservé aux nounous).", action="store_true") + parser.add_argument('-v', '--verbose', help="Permet de contourner les règles de choix du mot de passe (réservé aux nounous).", action="store_true") + parser.add_argument('user', type=str, nargs="?", help="L'utilisateur dont on veut changer le mot de passe.") + args = parser.parse_args() - if getuser() == login : - cprint(u'Utiliser passwd pour changer son propre mot de passe', 'rouge') - sys.exit(2) - - if self_mode : - s = commands.getoutput('sudo -u respbats ldap_whoami') - else : - s = commands.getoutput("/usr/bin/ldapsearch -D cn=readonly,dc=crans,dc=org -y/etc/ldap/readonly -x -LLL '(&(objectClass=posixAccount)(uid=%s))' dn nom prenom droits | grep -v MultiMachines" % login).strip() - if not s : - cprint(u'Login non trouvé dans la base LDAP', 'rouge') - sys.exit(3) - - # Ca a l'air bon - if s.find('\n\n') != -1 : - # Plusieurs trouvé : pas normal - cprint(u'Erreur lors de la recherche du login : plusieurs occurences !', 'rouge') - sys.exit(4) - - s = s.strip().split('\n') - dn = s[0].split()[1] - try : - if len(s) == 2 : - cprint(u"Changement du mot de passe du club %s "%decode64(' '.join(s[1].split()[1:])), 'vert') - else : - cprint(u"Changement du mot de passe de %s %s " % ( s[2].split()[1], s[1].split()[1] ), 'vert') - except : - cprint(u'Erreur lors de la recherche du login', 'rouge') - sys.exit(5) - - if self_mode : - # Il faut vérifier l'ancien mot de passe - ldap_auth_dn = dn - ldap_password = getpass.getpass('Mot de passe actuel : ') - s = commands.getoutput("/usr/bin/ldapwhoami -H '%s' -x -D '%s' -w '%s'" % ( uri, ldap_auth_dn, ldap_password ) ).strip() - try : - resultat = s.split('\n')[0].split(':')[1].strip() - except : - cprint(u"Erreur lors de l'authentification", 'rouge') - sys.exit(7) - if resultat != dn : - cprint({ 'Invalid credentials (49)': u'Mot de passe invalide' }.get(resultat, resultat), 'rouge') - sys.exit(8) - - elif len(s) > 3 and os.getuid()!=0 and not isadm(): - # Adhérent avec droits et on est pas root - From = 'roots@crans.org' - To = 'roots@crans.org' - mail = """From: Root <%s> -To: %s -Subject: Tentative de changement de mot de passe ! - -Tentative de changement du mot de passe de %s par %s. -""" % ( From, To , login, os.getlogin() ) - - # Envoi mail - import smtplib - conn = smtplib.SMTP('localhost') - conn.sendmail(From, To , mail ) - conn.quit() - cprint(u'Impossible de changer le mot de passe de cet adhérent : compte privilégié', 'rouge') - sys.exit(6) - - # Finalement ! - chgpass(dn) + if args.help: + parser.print_help() + sys.exit(0) + if args.no_cracklib: + if not lc_ldap.attributs.nounou in ldap.droits: + args.no_cracklib = False + change_password(args) diff --git a/gestion/config/password.py b/gestion/config/password.py new file mode 100644 index 00000000..731fddeb --- /dev/null +++ b/gestion/config/password.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Longueur minimale quand on appelle le script en étant nounou avec +# le bon argument +root_min_len = 4 + +# Longueur minimale standard +min_len = 9 + +# Nombre minimal de chiffres requis +min_cif = 1 + +# Nombre minimal de minuscules requises +min_low = 1 + +# Nombre minimal de majuscules requises +min_upp = 1 + +# Nombre minimal d'autres caractères requis +min_oth = 1 + +# Valeur des majuscules +upp_value = 1 + +# Valeur des minuscules +low_value = 1 + +# Valeur des chiffres +cif_value = 1 + +# Valeur des autres caractères +oth_value = 2