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">&times;</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