diff --git a/data/users.py b/data/users.py index f8fce77a5..bcd0fad62 100644 --- a/data/users.py +++ b/data/users.py @@ -1,7 +1,10 @@ import ldap import logging +import json +import itertools +import uuid -from flask.sessions import SecureCookieSessionInterface, BadSignature +from util.aes import AESCipher from util.validation import generate_valid_usernames from data import model @@ -107,6 +110,7 @@ class LDAPUsers(object): return found_user is not None + class UserAuthentication(object): def __init__(self, app=None): self.app = app @@ -139,23 +143,68 @@ class UserAuthentication(object): app.extensions['authentication'] = users return users + def _get_secret_key(self): + """ Returns the secret key to use for encrypting and decrypting. """ + from app import app + app_secret_key = app.config['SECRET_KEY'] + + # First try parsing the key as a float. + try: + secret_key = float(app_secret_key) + except ValueError: + secret_key = app_secret_key + + # Next try parsing it as an UUID. + try: + secret_key = uuid.UUID(app_secret_key).bytes + except ValueError: + secret_key = app_secret_key + + # Otherwise, use the bytes directly. + return ''.join(itertools.islice(itertools.cycle(secret_key), 32)) + + def encrypt_user_password(self, password): + """ Returns an encrypted version of the user's password. """ + data = { + 'password': password + } + + message = json.dumps(data) + cipher = AESCipher(self._get_secret_key()) + return cipher.encrypt(message) + + def _decrypt_user_password(self, encrypted): + """ Attempts to decrypt the given password and returns it. """ + cipher = AESCipher(self._get_secret_key()) + + try: + message = cipher.decrypt(encrypted) + except ValueError: + return None + except TypeError: + return None + + try: + data = json.loads(message) + except ValueError: + return None + + return data.get('password', encrypted) + def verify_user(self, username_or_email, password, basic_auth=False): # First try to decode the password as a signed token. if basic_auth: - from app import app import features - ser = SecureCookieSessionInterface().get_signing_serializer(app) - - try: - token_data = ser.loads(password) - password = token_data.get('password', password) - except BadSignature: + decrypted = self._decrypt_user_password(password) + if decrypted is None: # This is a normal password. if features.REQUIRE_ENCRYPTED_BASIC_AUTH: msg = ('Client login with passwords is disabled. Please generate a client token ' + 'and use it in place of your password.') return (None, msg) + else: + password = decrypted result = self.state.verify_user(username_or_email, password) if result: diff --git a/endpoints/api/user.py b/endpoints/api/user.py index d10807940..9ccb1d7aa 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -5,7 +5,6 @@ from random import SystemRandom from flask import request from flask.ext.login import logout_user from flask.ext.principal import identity_changed, AnonymousIdentity -from flask.sessions import SecureCookieSessionInterface from peewee import IntegrityError from app import app, billing as stripe, authentication, avatar @@ -370,15 +369,8 @@ class ClientKey(ApiResource): if not result: raise request_error(message=error_message) - ser = SecureCookieSessionInterface().get_signing_serializer(app) - data_to_sign = { - 'password': password, - 'nonce': SystemRandom().randint(0, 10000000000) - } - - encrypted = ser.dumps(data_to_sign) return { - 'key': encrypted + 'key': authentication.encrypt_user_password(password) } diff --git a/static/directives/config/config-setup-tool.html b/static/directives/config/config-setup-tool.html index 9be7ddd37..aa0c60e5d 100644 --- a/static/directives/config/config-setup-tool.html +++ b/static/directives/config/config-setup-tool.html @@ -311,13 +311,13 @@
- It is highly recommended to require encrypted client tokens. LDAP passwords used in the Docker client will be stored in plain-text! + It is highly recommended to require encrypted client tokens. LDAP passwords used in the Docker client will be stored in plaintext! Enable this requirement now.
Note: The "Require Encrypted Client Tokens" feature is currently enabled which will - prevent LDAP passwords from being saved as plain-text by the Docker client. + prevent LDAP passwords from being saved as plaintext by the Docker client.
diff --git a/static/js/pages/user-admin.js b/static/js/pages/user-admin.js index 43a56388a..8be13df30 100644 --- a/static/js/pages/user-admin.js +++ b/static/js/pages/user-admin.js @@ -208,7 +208,7 @@ }, ApiService.errorDisplay('Could not generate token')); }; - UIService.showPasswordDialog('Enter your password to generate a client token:', generateToken); + UIService.showPasswordDialog('Enter your password to generated an encrypted version:', generateToken); }; $scope.detachExternalLogin = function(kind) { diff --git a/static/partials/user-admin.html b/static/partials/user-admin.html index 4d803689d..9dee0f5ca 100644 --- a/static/partials/user-admin.html +++ b/static/partials/user-admin.html @@ -32,8 +32,7 @@
  • Account E-mail
  • Robot Accounts
  • -
  • Change Password
  • -
  • Client Token
  • +
  • Password
  • External Logins
  • Authorized Applications
  • @@ -100,16 +99,6 @@ - -
    -
    Click the "Generate" button below to generate a client token that can be used in place of your password for the Docker - command line.
    - - -
    -
    @@ -150,8 +139,31 @@
    - +
    + +
    +
    +
    Generate Encrypted Password
    + +
    +
    + Due to Docker storing passwords entered on the command line in plaintext, it is highly recommended to use the button below to generate an an encrypted version of your password. +
    + +
    + This installation is set to require encrypted passwords when + using the Docker command line interface. To generate an encrypted password, click the button below. +
    + + +
    +
    +
    + +
    Change Password
    @@ -163,6 +175,9 @@ Password changed successfully
    +
    Note: Changing your password will also invalidate any generated encrypted passwords.
    + +
    -