diff --git a/debian/control b/debian/control index 8880867a2..df9a6d2bd 100644 --- a/debian/control +++ b/debian/control @@ -15,7 +15,7 @@ Depends: ${python3:Depends}, ${misc:Depends} , python3-miniupnpc, python3-dbus, python3-jinja2 , python3-toml, python3-packaging, python3-publicsuffix2 , python3-ldap, python3-zeroconf (>= 0.36), python3-lexicon, - , python3-jwt + , python3-cryptography, python3-jwt , python-is-python3 , nginx, nginx-extras (>=1.18) , apt, apt-transport-https, apt-utils, dirmngr diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index 7bd835f8f..4d53997a5 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -177,7 +177,8 @@ do_post_regen() { getent passwd ynh-portal &>/dev/null || useradd --no-create-home --shell /usr/sbin/nologin --system --user-group ynh-portal if [ ! -e /etc/yunohost/.ssowat_cookie_secret ]; then - dd if=/dev/urandom bs=1 count=1000 2>/dev/null | tr --complement --delete 'A-Za-z0-9' | head -c 64 > /etc/yunohost/.ssowat_cookie_secret + # NB: we need this to be exactly 32 char long, because it is later used as a key for AES256 + dd if=/dev/urandom bs=1 count=1000 2>/dev/null | tr --complement --delete 'A-Za-z0-9' | head -c 32 > /etc/yunohost/.ssowat_cookie_secret fi chown ynh-portal:root /etc/yunohost/.ssowat_cookie_secret chmod 400 /etc/yunohost/.ssowat_cookie_secret diff --git a/src/authenticators/ldap_ynhuser.py b/src/authenticators/ldap_ynhuser.py index c8ba4ecf2..407277acf 100644 --- a/src/authenticators/ldap_ynhuser.py +++ b/src/authenticators/ldap_ynhuser.py @@ -5,6 +5,13 @@ import logging import ldap import ldap.sasl import datetime +import base64 +import os + +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives import padding +from cryptography.hazmat.backends import default_backend + from moulinette import m18n from moulinette.authentication import BaseAuthenticator @@ -13,13 +20,52 @@ from yunohost.utils.error import YunohostError, YunohostAuthenticationError # FIXME : we shall generate this somewhere if it doesnt exists yet # FIXME : fix permissions -session_secret = open("/etc/yunohost/.ssowat_cookie_secret").read() +session_secret = open("/etc/yunohost/.ssowat_cookie_secret").read().strip() logger = logging.getLogger("yunohostportal.authenticators.ldap_ynhuser") URI = "ldap://localhost:389" USERDN = "uid={username},ou=users,dc=yunohost,dc=org" +# We want to save the password in the cookie, but we should do so in an encrypted fashion +# This is needed because the SSO later needs to possibly inject the Basic Auth header +# which includes the user's password +# It's also needed because we need to be able to open LDAP sessions, authenticated as the user, +# which requires the user's password +# +# To do so, we use AES-256-CBC. As it's a block encryption algorithm, it requires an IV, +# which we need to keep around for decryption on SSOwat'side. +# +# session_secret is used as the encryption key, which implies it must be exactly 32-char long (256/8) +# +# The result is a string formatted as | +# For example: ctl8kk5GevYdaA5VZ2S88Q==|yTAzCx0Gd1+MCit4EQl9lA== +def encrypt(data): + + alg = algorithms.AES(session_secret.encode()) + iv = os.urandom(int(alg.block_size / 8)) + + E = Cipher(alg, modes.CBC(iv), default_backend()).encryptor() + p = padding.PKCS7(alg.block_size).padder() + data_padded = p.update(data.encode()) + p.finalize() + data_enc = E.update(data_padded) + E.finalize() + data_enc_b64 = base64.b64encode(data_enc).decode() + iv_b64 = base64.b64encode(iv).decode() + return data_enc_b64 + "|" + iv_b64 + +def decrypt(data_enc_and_iv_b64): + + data_enc_b64, iv_b64 = data_enc_and_iv_b64.split("|") + data_enc = base64.b64decode(data_enc_b64) + iv = base64.b64decode(iv_b64) + + alg = algorithms.AES(session_secret.encode()) + D = Cipher(alg, modes.CBC(iv), default_backend()).decryptor() + p = padding.PKCS7(alg.block_size).unpadder() + data_padded = D.update(data_enc) + data = p.update(data_padded) + p.finalize() + return data.decode() + class Authenticator(BaseAuthenticator): @@ -64,23 +110,7 @@ class Authenticator(BaseAuthenticator): if con: con.unbind_s() - - - - - - - # FIXME FIXME FIXME : the password is to be encrypted to not expose it in the JWT cookie which is only signed and base64 encoded but not encrypted - - - - - - - - - - return {"user": username, "password": password} + return {"user": username, "pwd": encrypt(password)} def set_session_cookie(self, infos): @@ -101,7 +131,7 @@ class Authenticator(BaseAuthenticator): response.set_cookie( "yunohost.portal", - jwt.encode(new_infos, session_secret, algorithm="HS256").decode(), + jwt.encode(new_infos, session_secret, algorithm="HS256"), secure=True, httponly=True, path="/", @@ -109,7 +139,7 @@ class Authenticator(BaseAuthenticator): # FIXME : add Expire clause ) - def get_session_cookie(self, raise_if_no_session_exists=True): + def get_session_cookie(self, raise_if_no_session_exists=True, decrypt_pwd=False): from bottle import request @@ -127,6 +157,9 @@ class Authenticator(BaseAuthenticator): if "id" not in infos: infos["id"] = random_ascii() + if decrypt_pwd: + infos["pwd"] = decrypt(infos["pwd"]) + # FIXME: Here, maybe we want to re-authenticate the session via the authenticator # For example to check that the username authenticated is still in the admin group... diff --git a/src/portal.py b/src/portal.py index 2eaa59dd4..6aa7cba6d 100644 --- a/src/portal.py +++ b/src/portal.py @@ -32,24 +32,17 @@ logger = getActionLogger("yunohostportal.user") def portal_me(): """ Get user informations - - Keyword argument: - username -- Username to get informations - """ - import pdb; pdb.set_trace() - - auth = Auth().get_session_cookie() + auth = Auth().get_session_cookie(decrypt_pwd=True) username = auth["user"] - password = auth["password"] - ldap = LDAPInterface(username, password) + ldap = LDAPInterface(username, auth["pwd"]) user_attrs = ["cn", "mail", "uid", "maildrop", "givenName", "sn", "mailuserquota"] filter = "uid=" + username - result = ldap.search("ou=users,dc=yunohost,dc=org", filter, user_attrs) + result = ldap.search("ou=users", filter, user_attrs) if result: user = result[0]