From aaf1b23e98bc7643bdb75c99eef097f51b10d879 Mon Sep 17 00:00:00 2001 From: Joseph Schorr <joseph.schorr@coreos.com> Date: Thu, 26 Mar 2015 15:10:58 -0400 Subject: [PATCH] Address CL concerns and switch to a real encryption system --- data/users.py | 65 ++++++++++++++++--- endpoints/api/user.py | 10 +-- .../directives/config/config-setup-tool.html | 4 +- static/js/pages/user-admin.js | 2 +- static/partials/user-admin.html | 48 +++++++++----- util/aes.py | 32 +++++++++ 6 files changed, 124 insertions(+), 37 deletions(-) create mode 100644 util/aes.py 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 @@ </div> <div class="alert alert-warning" ng-if="config.AUTHENTICATION_TYPE == 'LDAP' && !config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH"> - It is <strong>highly recommended</strong> to require encrypted client tokens. LDAP passwords used in the Docker client will be stored in <strong>plain-text</strong>! + It is <strong>highly recommended</strong> to require encrypted client tokens. LDAP passwords used in the Docker client will be stored in <strong>plaintext</strong>! <a href="javascript:void(0)" ng-click="config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH = true">Enable this requirement now</a>. </div> <div class="alert alert-success" ng-if="config.AUTHENTICATION_TYPE == 'LDAP' && config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH"> 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. </div> <table class="config-table"> 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 @@ <!-- Non-billing --> <li quay-classes="{'!Features.BILLING': 'active'}"><a href="javascript:void(0)" data-toggle="tab" data-target="#email">Account E-mail</a></li> <li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li> - <li><a href="javascript:void(0)" data-toggle="tab" data-target="#password">Change Password</a></li> - <li><a href="javascript:void(0)" data-toggle="tab" data-target="#clienttoken">Client Token</a></li> + <li><a href="javascript:void(0)" data-toggle="tab" data-target="#password">Password</a></li> <li><a href="javascript:void(0)" data-toggle="tab" data-target="#external" quay-show="Features.GITHUB_LOGIN || Features.GOOGLE_LOGIN">External Logins</a></li> <li><a href="javascript:void(0)" data-toggle="tab" data-target="#authorized" ng-click="loadAuthedApps()">Authorized Applications</a></li> <li quay-show="Features.USER_LOG_ACCESS || hasPaidBusinessPlan"> @@ -100,16 +99,6 @@ </div> - <!-- Client token tab --> - <div id="clienttoken" class="tab-pane"> - <div class="alert alert-success">Click the "Generate" button below to generate a client token that can be used <strong>in place of your password</strong> for the Docker - command line.</div> - - <button class="btn btn-primary" ng-click="generateClientToken()"> - <i class="fa fa-key" style="margin-right: 6px;"></i>Generate Client Token - </button> - </div> - <!-- Logs tab --> <div id="logs" class="tab-pane"> <div class="logs-view" user="user" makevisible="logsShown"></div> @@ -150,8 +139,31 @@ </div> </div> - <!-- Change password tab --> + <!-- Password tab --> <div id="password" class="tab-pane"> + <!-- Encrypted Password --> + <div class="row"> + <div class="panel"> + <div class="panel-title">Generate Encrypted Password</div> + + <div class="panel-body"> + <div class="alert alert-info" ng-if="!Features.REQUIRE_ENCRYPTED_BASIC_AUTH"> + Due to Docker storing passwords entered on the command line in <strong>plaintext</strong>, it is highly recommended to use the button below to generate an an encrypted version of your password. + </div> + + <div class="alert alert-warning" ng-if="Features.REQUIRE_ENCRYPTED_BASIC_AUTH"> + This installation is set to <strong>require</strong> encrypted passwords when + using the Docker command line interface. To generate an encrypted password, click the button below. + </div> + + <button class="btn btn-primary" ng-click="generateClientToken()"> + <i class="fa fa-key" style="margin-right: 6px;"></i>Generate Encrypted Password + </button> + </div> + </div> + </div> + + <!-- Change Password --> <div class="row"> <div class="panel"> <div class="panel-title">Change Password</div> @@ -163,6 +175,9 @@ <span class="help-block" ng-show="changePasswordSuccess">Password changed successfully</span> <div ng-show="!updatingUser" class="panel-body"> + <div class="alert alert-warning">Note: Changing your password will also invalidate any generated encrypted passwords.</div> + + <form class="form-change col-md-6" id="changePasswordForm" name="changePasswordForm" ng-submit="changePassword()" ng-show="!awaitingConfirmation && !registering"> <input type="password" class="form-control" placeholder="Your new password" ng-model="cuser.password" required @@ -387,21 +402,20 @@ <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h4 class="modal-title">Generate Client Token</h4> + <h4 class="modal-title">Encrypted Password</h4> </div> <div class="modal-body"> - <div style="margin-bottom: 10px;">Your generated client token:</div> + <div style="margin-bottom: 10px;">Your generated encrypted password:</div> <div class="copy-box" value="generatedClientToken"></div> </div> <div class="modal-footer"> - <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> + <button type="button" class="btn btn-default" data-dismiss="modal">Dismiss</button> </div> </div><!-- /.modal-content --> </div><!-- /.modal-dialog --> </div><!-- /.modal --> - <!-- Modal message dialog --> <div class="modal fade" id="reallyconvertModal"> <div class="modal-dialog"> diff --git a/util/aes.py b/util/aes.py new file mode 100644 index 000000000..10f1ac030 --- /dev/null +++ b/util/aes.py @@ -0,0 +1,32 @@ +import base64 +import hashlib +from Crypto import Random +from Crypto.Cipher import AES + +class AESCipher(object): + """ Helper class for encrypting and decrypting data via AES. + + Copied From: http://stackoverflow.com/a/21928790 + """ + def __init__(self, key): + self.bs = 32 + self.key = key + + def encrypt(self, raw): + raw = self._pad(raw) + iv = Random.new().read(AES.block_size) + cipher = AES.new(self.key, AES.MODE_CBC, iv) + return base64.b64encode(iv + cipher.encrypt(raw)) + + def decrypt(self, enc): + enc = base64.b64decode(enc) + iv = enc[:AES.block_size] + cipher = AES.new(self.key, AES.MODE_CBC, iv) + return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8') + + def _pad(self, s): + return s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs) + + @staticmethod + def _unpad(s): + return s[:-ord(s[len(s)-1:])] \ No newline at end of file