mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
portalapi: implement encrypted password storage in the user's cookie using AES256
This commit is contained in:
parent
9a5080ea16
commit
6c6dd318fb
4 changed files with 59 additions and 32 deletions
2
debian/control
vendored
2
debian/control
vendored
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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...
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
Loading…
Add table
Reference in a new issue