portalapi: implement encrypted password storage in the user's cookie using AES256

This commit is contained in:
Alexandre Aubin 2023-07-11 22:39:22 +02:00
parent 9a5080ea16
commit 6c6dd318fb
4 changed files with 59 additions and 32 deletions

2
debian/control vendored
View file

@ -15,7 +15,7 @@ Depends: ${python3:Depends}, ${misc:Depends}
, python3-miniupnpc, python3-dbus, python3-jinja2 , python3-miniupnpc, python3-dbus, python3-jinja2
, python3-toml, python3-packaging, python3-publicsuffix2 , python3-toml, python3-packaging, python3-publicsuffix2
, python3-ldap, python3-zeroconf (>= 0.36), python3-lexicon, , python3-ldap, python3-zeroconf (>= 0.36), python3-lexicon,
, python3-jwt , python3-cryptography, python3-jwt
, python-is-python3 , python-is-python3
, nginx, nginx-extras (>=1.18) , nginx, nginx-extras (>=1.18)
, apt, apt-transport-https, apt-utils, dirmngr , apt, apt-transport-https, apt-utils, dirmngr

View file

@ -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 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 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 fi
chown ynh-portal:root /etc/yunohost/.ssowat_cookie_secret chown ynh-portal:root /etc/yunohost/.ssowat_cookie_secret
chmod 400 /etc/yunohost/.ssowat_cookie_secret chmod 400 /etc/yunohost/.ssowat_cookie_secret

View file

@ -5,6 +5,13 @@ import logging
import ldap import ldap
import ldap.sasl import ldap.sasl
import datetime 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 import m18n
from moulinette.authentication import BaseAuthenticator 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 : we shall generate this somewhere if it doesnt exists yet
# FIXME : fix permissions # 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") logger = logging.getLogger("yunohostportal.authenticators.ldap_ynhuser")
URI = "ldap://localhost:389" URI = "ldap://localhost:389"
USERDN = "uid={username},ou=users,dc=yunohost,dc=org" 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 <password_enc_b64>|<iv_b64>
# 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): class Authenticator(BaseAuthenticator):
@ -64,23 +110,7 @@ class Authenticator(BaseAuthenticator):
if con: if con:
con.unbind_s() con.unbind_s()
return {"user": username, "pwd": encrypt(password)}
# 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}
def set_session_cookie(self, infos): def set_session_cookie(self, infos):
@ -101,7 +131,7 @@ class Authenticator(BaseAuthenticator):
response.set_cookie( response.set_cookie(
"yunohost.portal", "yunohost.portal",
jwt.encode(new_infos, session_secret, algorithm="HS256").decode(), jwt.encode(new_infos, session_secret, algorithm="HS256"),
secure=True, secure=True,
httponly=True, httponly=True,
path="/", path="/",
@ -109,7 +139,7 @@ class Authenticator(BaseAuthenticator):
# FIXME : add Expire clause # 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 from bottle import request
@ -127,6 +157,9 @@ class Authenticator(BaseAuthenticator):
if "id" not in infos: if "id" not in infos:
infos["id"] = random_ascii() 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 # 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... # For example to check that the username authenticated is still in the admin group...

View file

@ -32,24 +32,17 @@ logger = getActionLogger("yunohostportal.user")
def portal_me(): def portal_me():
""" """
Get user informations Get user informations
Keyword argument:
username -- Username to get informations
""" """
import pdb; pdb.set_trace() auth = Auth().get_session_cookie(decrypt_pwd=True)
auth = Auth().get_session_cookie()
username = auth["user"] username = auth["user"]
password = auth["password"]
ldap = LDAPInterface(username, password) ldap = LDAPInterface(username, auth["pwd"])
user_attrs = ["cn", "mail", "uid", "maildrop", "givenName", "sn", "mailuserquota"] user_attrs = ["cn", "mail", "uid", "maildrop", "givenName", "sn", "mailuserquota"]
filter = "uid=" + username 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: if result:
user = result[0] user = result[0]