Address CL concerns and switch to a real encryption system

This commit is contained in:
Joseph Schorr 2015-03-26 15:10:58 -04:00
parent d23bb6616d
commit aaf1b23e98
6 changed files with 124 additions and 37 deletions

View file

@ -1,7 +1,10 @@
import ldap import ldap
import logging 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 util.validation import generate_valid_usernames
from data import model from data import model
@ -107,6 +110,7 @@ class LDAPUsers(object):
return found_user is not None return found_user is not None
class UserAuthentication(object): class UserAuthentication(object):
def __init__(self, app=None): def __init__(self, app=None):
self.app = app self.app = app
@ -139,23 +143,68 @@ class UserAuthentication(object):
app.extensions['authentication'] = users app.extensions['authentication'] = users
return 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): def verify_user(self, username_or_email, password, basic_auth=False):
# First try to decode the password as a signed token. # First try to decode the password as a signed token.
if basic_auth: if basic_auth:
from app import app
import features import features
ser = SecureCookieSessionInterface().get_signing_serializer(app) decrypted = self._decrypt_user_password(password)
if decrypted is None:
try:
token_data = ser.loads(password)
password = token_data.get('password', password)
except BadSignature:
# This is a normal password. # This is a normal password.
if features.REQUIRE_ENCRYPTED_BASIC_AUTH: if features.REQUIRE_ENCRYPTED_BASIC_AUTH:
msg = ('Client login with passwords is disabled. Please generate a client token ' + msg = ('Client login with passwords is disabled. Please generate a client token ' +
'and use it in place of your password.') 'and use it in place of your password.')
return (None, msg) return (None, msg)
else:
password = decrypted
result = self.state.verify_user(username_or_email, password) result = self.state.verify_user(username_or_email, password)
if result: if result:

View file

@ -5,7 +5,6 @@ from random import SystemRandom
from flask import request from flask import request
from flask.ext.login import logout_user from flask.ext.login import logout_user
from flask.ext.principal import identity_changed, AnonymousIdentity from flask.ext.principal import identity_changed, AnonymousIdentity
from flask.sessions import SecureCookieSessionInterface
from peewee import IntegrityError from peewee import IntegrityError
from app import app, billing as stripe, authentication, avatar from app import app, billing as stripe, authentication, avatar
@ -370,15 +369,8 @@ class ClientKey(ApiResource):
if not result: if not result:
raise request_error(message=error_message) 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 { return {
'key': encrypted 'key': authentication.encrypt_user_password(password)
} }

View file

@ -311,13 +311,13 @@
</div> </div>
<div class="alert alert-warning" ng-if="config.AUTHENTICATION_TYPE == 'LDAP' && !config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH"> <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>. <a href="javascript:void(0)" ng-click="config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH = true">Enable this requirement now</a>.
</div> </div>
<div class="alert alert-success" ng-if="config.AUTHENTICATION_TYPE == 'LDAP' && config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH"> <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 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> </div>
<table class="config-table"> <table class="config-table">

View file

@ -208,7 +208,7 @@
}, ApiService.errorDisplay('Could not generate token')); }, 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) { $scope.detachExternalLogin = function(kind) {

View file

@ -32,8 +32,7 @@
<!-- Non-billing --> <!-- 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 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="#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="#password">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="#external" quay-show="Features.GITHUB_LOGIN || Features.GOOGLE_LOGIN">External Logins</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><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"> <li quay-show="Features.USER_LOG_ACCESS || hasPaidBusinessPlan">
@ -100,16 +99,6 @@
</div> </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 --> <!-- Logs tab -->
<div id="logs" class="tab-pane"> <div id="logs" class="tab-pane">
<div class="logs-view" user="user" makevisible="logsShown"></div> <div class="logs-view" user="user" makevisible="logsShown"></div>
@ -150,8 +139,31 @@
</div> </div>
</div> </div>
<!-- Change password tab --> <!-- Password tab -->
<div id="password" class="tab-pane"> <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="row">
<div class="panel"> <div class="panel">
<div class="panel-title">Change Password</div> <div class="panel-title">Change Password</div>
@ -163,6 +175,9 @@
<span class="help-block" ng-show="changePasswordSuccess">Password changed successfully</span> <span class="help-block" ng-show="changePasswordSuccess">Password changed successfully</span>
<div ng-show="!updatingUser" class="panel-body"> <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()" <form class="form-change col-md-6" id="changePasswordForm" name="changePasswordForm" ng-submit="changePassword()"
ng-show="!awaitingConfirmation && !registering"> ng-show="!awaitingConfirmation && !registering">
<input type="password" class="form-control" placeholder="Your new password" ng-model="cuser.password" required <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-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Generate Client Token</h4> <h4 class="modal-title">Encrypted Password</h4>
</div> </div>
<div class="modal-body"> <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 class="copy-box" value="generatedClientToken"></div>
</div> </div>
<div class="modal-footer"> <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>
</div><!-- /.modal-content --> </div><!-- /.modal-content -->
</div><!-- /.modal-dialog --> </div><!-- /.modal-dialog -->
</div><!-- /.modal --> </div><!-- /.modal -->
<!-- Modal message dialog --> <!-- Modal message dialog -->
<div class="modal fade" id="reallyconvertModal"> <div class="modal fade" id="reallyconvertModal">
<div class="modal-dialog"> <div class="modal-dialog">

32
util/aes.py Normal file
View file

@ -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:])]