Address CL concerns and switch to a real encryption system
This commit is contained in:
parent
d23bb6616d
commit
aaf1b23e98
6 changed files with 124 additions and 37 deletions
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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">×</button>
|
<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>
|
||||||
<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
32
util/aes.py
Normal 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:])]
|
Reference in a new issue