Просмотр исходного кода

client side token creation, handle display and single password retry

El RIDO 7 лет назад
Родитель
Сommit
5651c0f04e
9 измененных файлов с 128 добавлено и 61 удалено
  1. 113 48
      js/privatebin.js
  2. 1 0
      lib/Filter.php
  3. 1 2
      lib/FormatV2.php
  4. 3 3
      lib/Model/Paste.php
  5. 1 1
      tpl/bootstrap.php
  6. 1 1
      tpl/page.php
  7. 2 2
      tst/ControllerTest.php
  8. 3 1
      tst/FormatV2Test.php
  9. 3 3
      tst/ModelTest.php

+ 113 - 48
js/privatebin.js

@@ -664,6 +664,23 @@ jQuery.PrivateBin = (function($, RawDeflate) {
          */
          */
         let base58 = new baseX('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
         let base58 = new baseX('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
 
 
+        /**
+         * convert hexadecimal string to binary representation
+         *
+         * @name   CryptTool.hex2bin
+         * @function
+         * @private
+         * @param  {string} message hex string
+         * @return {string} binary representation as a DOMString
+         */
+        function hex2bin(message) {
+            let result = [];
+            for (let i = 0, l = message.length; i < l; i += 2) {
+                result.push(parseInt(message.substr(i, 2), 16));
+            }
+            return String.fromCharCode.apply(String, result);
+        }
+
         /**
         /**
          * convert UTF-8 string stored in a DOMString to a standard UTF-16 DOMString
          * convert UTF-8 string stored in a DOMString to a standard UTF-16 DOMString
          *
          *
@@ -906,6 +923,33 @@ jQuery.PrivateBin = (function($, RawDeflate) {
             );
             );
         }
         }
 
 
+        /**
+         * derive PBKDF2 protected credentials for server to validate password
+         *
+         * @name   CryptTool.deriveCredentials
+         * @function
+         * @private
+         * @param  {string} key
+         * @param  {string} password
+         * @return {string} derived key
+         */
+        async function deriveCredentials(key, password)
+        {
+            const spec = [
+                null,               // initialization vector
+                key.slice(0, 16),   // salt
+                100000,             // iterations
+                256,                // key size
+                null,               // tag size
+                null,               // algorithm
+                'gcm',              // algorithm mode
+                'none'              // compression
+            ];
+            return window.crypto.subtle.exportKey(
+                'raw', await deriveKey(key.slice(16), password, spec, true)
+            );
+        }
+
         /**
         /**
          * gets crypto settings from specification and authenticated data
          * gets crypto settings from specification and authenticated data
          *
          *
@@ -933,25 +977,47 @@ jQuery.PrivateBin = (function($, RawDeflate) {
          * @function
          * @function
          * @param  {string} key
          * @param  {string} key
          * @param  {string} password
          * @param  {string} password
-         * @return {string} decrypted message, empty if decryption failed
+         * @return {string} derived key
          */
          */
         me.getCredentials = async function(key, password)
         me.getCredentials = async function(key, password)
         {
         {
-            const spec = [
-                null,               // initialization vector
-                key.slice(0, 16),   // salt
-                100000,             // iterations
-                256,                // key size
-                null,               // tag size
-                null,               // algorithm
-                'gcm',              // algorithm mode
-                'none'              // compression
-            ];
-            key = key.slice(16);
-            let derivedKey = await deriveKey(key, password, spec, true);
             return btoa(
             return btoa(
                 arraybufferToString(
                 arraybufferToString(
-                    await window.crypto.subtle.exportKey('raw', derivedKey)
+                    await deriveCredentials(key, password)
+                )
+            );
+        }
+
+        /**
+         * get HMAC of paste ID and PBKDF2 protected credentials for server to validate
+         *
+         * @name   CryptTool.getToken
+         * @function
+         * @param  {string} id
+         * @param  {string} key
+         * @param  {string} password
+         * @return {string} decrypted message, empty if decryption failed
+         */
+        me.getToken = async function(id, key, password)
+        {
+            return btoa(
+                arraybufferToString(
+                    await window.crypto.subtle.sign(
+                        {name: 'HMAC'},
+                        await window.crypto.subtle.importKey(
+                            'raw',
+                            await deriveCredentials(key, password),
+                            {
+                                name: 'HMAC',
+                                hash: {name: 'SHA-256'} // can be "SHA-1", "SHA-256", "SHA-384" or "SHA-512"
+                            },
+                            false, // may not export this
+                            ['sign']
+                        ),
+                        stringToArraybuffer(
+                            hex2bin(id)
+                        )
+                    )
                 )
                 )
             );
             );
         }
         }
@@ -1160,7 +1226,7 @@ jQuery.PrivateBin = (function($, RawDeflate) {
          *                            force a data reload. Default: true
          *                            force a data reload. Default: true
          * @return string
          * @return string
          */
          */
-        me.getPasteData = function(callback, useCache)
+        me.getPasteData = async function(callback, useCache)
         {
         {
             // use cache if possible/allowed
             // use cache if possible/allowed
             if (useCache !== false && pasteData !== null) {
             if (useCache !== false && pasteData !== null) {
@@ -1173,17 +1239,31 @@ jQuery.PrivateBin = (function($, RawDeflate) {
                 return pasteData;
                 return pasteData;
             }
             }
 
 
-            // reload data
+            // load data
             ServerInteraction.prepare();
             ServerInteraction.prepare();
-            ServerInteraction.setUrl(Helper.baseUri() + '?pasteid=' + me.getPasteId());
+            ServerInteraction.setUrl(
+                Helper.baseUri() + '?' + $.param({
+                    pasteid: me.getPasteId(),
+                    token: await CryptTool.getToken(
+                        me.getPasteId(), me.getPasteKey(), Prompt.getPassword()
+                    )
+                })
+            );
 
 
             ServerInteraction.setFailure(function (status, data) {
             ServerInteraction.setFailure(function (status, data) {
                 // revert loading status…
                 // revert loading status…
                 Alert.hideLoading();
                 Alert.hideLoading();
                 TopNav.showViewButtons();
                 TopNav.showViewButtons();
 
 
-                // show error message
-                Alert.showError(ServerInteraction.parseUploadError(status, data, 'get paste data'));
+                // might be a missing password, try one more time after getting one
+                if (Prompt.getPassword().length === 0) {
+                    Prompt.requestPassword(function () {
+                        me.getPasteData(callback, useCache);
+                    });
+                } else {
+                    // show error message
+                    Alert.showError(ServerInteraction.parseUploadError(status, data, 'get paste data'));
+                }
             });
             });
             ServerInteraction.setSuccess(function (status, data) {
             ServerInteraction.setSuccess(function (status, data) {
                 pasteData = new Paste(data);
                 pasteData = new Paste(data);
@@ -1909,8 +1989,9 @@ jQuery.PrivateBin = (function($, RawDeflate) {
          *
          *
          * @name Prompt.requestPassword
          * @name Prompt.requestPassword
          * @function
          * @function
+         * @param {function} callback
          */
          */
-        me.requestPassword = function()
+        me.requestPassword = function(callback)
         {
         {
             // show new bootstrap method (if available)
             // show new bootstrap method (if available)
             if ($passwordModal.length !== 0) {
             if ($passwordModal.length !== 0) {
@@ -1928,9 +2009,9 @@ jQuery.PrivateBin = (function($, RawDeflate) {
             }
             }
             if (password.length === 0) {
             if (password.length === 0) {
                 // recurse…
                 // recurse…
-                return me.requestPassword();
+                return me.requestPassword(callback);
             }
             }
-            PasteDecrypter.run();
+            callback();
         };
         };
 
 
         /**
         /**
@@ -4087,7 +4168,7 @@ jQuery.PrivateBin = (function($, RawDeflate) {
             // show notification
             // show notification
             const baseUri   = Helper.baseUri() + '?',
             const baseUri   = Helper.baseUri() + '?',
                   url       = baseUri + data.id + '#' + CryptTool.base58encode(data.encryptionKey),
                   url       = baseUri + data.id + '#' + CryptTool.base58encode(data.encryptionKey),
-                  deleteUrl = baseUri + 'pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
+                  deleteUrl = baseUri + $.param({pasteid: data.id, deletetoken: data.deletetoken});
             PasteStatus.createPasteNotification(url, deleteUrl);
             PasteStatus.createPasteNotification(url, deleteUrl);
 
 
             // show new URL in browser bar
             // show new URL in browser bar
@@ -4254,7 +4335,7 @@ jQuery.PrivateBin = (function($, RawDeflate) {
             ]);
             ]);
             ServerInteraction.setUnencryptedData('meta', {
             ServerInteraction.setUnencryptedData('meta', {
                 'expire':    TopNav.getExpiration(),
                 'expire':    TopNav.getExpiration(),
-                'challenge': CryptTool.getCredentials(key, password)
+                'challenge': await CryptTool.getCredentials(key, password)
             });
             });
 
 
             // prepare PasteViewer for later preview
             // prepare PasteViewer for later preview
@@ -4318,7 +4399,7 @@ jQuery.PrivateBin = (function($, RawDeflate) {
             // if it fails, request password
             // if it fails, request password
             if (plaindata.length === 0 && password.length === 0) {
             if (plaindata.length === 0 && password.length === 0) {
                 // show prompt
                 // show prompt
-                Prompt.requestPassword();
+                Prompt.requestPassword(me.run);
 
 
                 // Thus, we cannot do anything yet, we need to wait for the user
                 // Thus, we cannot do anything yet, we need to wait for the user
                 // input.
                 // input.
@@ -4764,31 +4845,15 @@ jQuery.PrivateBin = (function($, RawDeflate) {
             const orgPosition = $(window).scrollTop();
             const orgPosition = $(window).scrollTop();
 
 
             Model.getPasteData(function (data) {
             Model.getPasteData(function (data) {
-                ServerInteraction.prepare();
-                ServerInteraction.setUrl(Helper.baseUri() + '?pasteid=' + Model.getPasteId());
+                PasteDecrypter.run(new Paste(data));
 
 
-                ServerInteraction.setFailure(function (status, data) {
-                    // revert loading status…
-                    Alert.hideLoading();
-                    TopNav.showViewButtons();
+                // restore position
+                window.scrollTo(0, orgPosition);
 
 
-                    // show error message
-                    Alert.showError(
-                        ServerInteraction.parseUploadError(status, data, 'refresh display')
-                    );
-                });
-                ServerInteraction.setSuccess(function (status, data) {
-                    PasteDecrypter.run(new Paste(data));
-
-                    // restore position
-                    window.scrollTo(0, orgPosition);
-
-                    // NOTE: could create problems as callback may be called
-                    // asyncronously if PasteDecrypter e.g. needs to wait for a
-                    // password being entered
-                    callback();
-                });
-                ServerInteraction.run();
+                // NOTE: could create problems as callback may be called
+                // asyncronously if PasteDecrypter e.g. needs to wait for a
+                // password being entered
+                callback();
             }, false); // this false is important as it circumvents the cache
             }, false); // this false is important as it circumvents the cache
         }
         }
 
 

+ 1 - 0
lib/Filter.php

@@ -72,6 +72,7 @@ class Filter
     /**
     /**
      * fixed time string comparison operation to prevent timing attacks
      * fixed time string comparison operation to prevent timing attacks
      * https://crackstation.net/hashing-security.htm?=rd#slowequals
      * https://crackstation.net/hashing-security.htm?=rd#slowequals
+     * can be replaced with hash_equals() after we drop PHP 5.5 support
      *
      *
      * @access public
      * @access public
      * @static
      * @static

+ 1 - 2
lib/FormatV2.php

@@ -123,8 +123,7 @@ class FormatV2
         // require only the key 'expire' in the metadata of pastes
         // require only the key 'expire' in the metadata of pastes
         if (!$isComment && (
         if (!$isComment && (
             count($message['meta']) === 0 ||
             count($message['meta']) === 0 ||
-            !array_key_exists('expire', $message['meta']) ||
-            count($message['meta']) > 1
+            !array_key_exists('expire', $message['meta'])
         )) {
         )) {
             return false;
             return false;
         }
         }

+ 3 - 3
lib/Model/Paste.php

@@ -116,9 +116,9 @@ class Paste extends AbstractModel
         $this->_data['meta']['salt']    = serversalt::generate();
         $this->_data['meta']['salt']    = serversalt::generate();
         // if a challenge was sent, we store the HMAC of paste ID & challenge
         // if a challenge was sent, we store the HMAC of paste ID & challenge
         if (array_key_exists('challenge', $this->_data['meta'])) {
         if (array_key_exists('challenge', $this->_data['meta'])) {
-            $this->_data['meta']['challenge'] = hash_hmac(
-                'sha256', $this->getId(), base64_decode($this->_data['meta']['challenge'])
-            );
+            $this->_data['meta']['challenge'] = base64_encode(hash_hmac(
+                'sha256', hex2bin($this->getId()), base64_decode($this->_data['meta']['challenge']), true
+            ));
         }
         }
 
 
         // store paste
         // store paste

+ 1 - 1
tpl/bootstrap.php

@@ -71,7 +71,7 @@ if ($MARKDOWN):
 endif;
 endif;
 ?>
 ?>
 		<script type="text/javascript" data-cfasync="false" src="js/purify-1.0.11.js" integrity="sha512-p7UyJuyBkhMcMgE4mDsgK0Lz70OvetLefua1oXs1OujWv9gOxh4xy8InFux7bZ4/DAZsTmO4rgVwZW9BHKaTaw==" crossorigin="anonymous"></script>
 		<script type="text/javascript" data-cfasync="false" src="js/purify-1.0.11.js" integrity="sha512-p7UyJuyBkhMcMgE4mDsgK0Lz70OvetLefua1oXs1OujWv9gOxh4xy8InFux7bZ4/DAZsTmO4rgVwZW9BHKaTaw==" crossorigin="anonymous"></script>
-		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-tQGC7Jwx3TVRu69OGEmWl0K6lwDmgh5of3xU1Xbo4U0EY1Sv9qyUMQaOgGnMET3WMu9kbuZHsPnf7d9tTXhbsQ==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-o8Q/t6/gpmx6bQaHw3gru3cjOD5BLE/KdBKja73SllZo0/FuLvAjJ+40KhZ8ig/EpioP04etJtfTnNzF/isXow==" crossorigin="anonymous"></script>
 		<!--[if IE]>
 		<!--[if IE]>
 		<style type="text/css">body {padding-left:60px;padding-right:60px;} #ienotice {display:block;}</style>
 		<style type="text/css">body {padding-left:60px;padding-right:60px;} #ienotice {display:block;}</style>
 		<![endif]-->
 		<![endif]-->

+ 1 - 1
tpl/page.php

@@ -49,7 +49,7 @@ if ($MARKDOWN):
 endif;
 endif;
 ?>
 ?>
 		<script type="text/javascript" data-cfasync="false" src="js/purify-1.0.11.js" integrity="sha512-p7UyJuyBkhMcMgE4mDsgK0Lz70OvetLefua1oXs1OujWv9gOxh4xy8InFux7bZ4/DAZsTmO4rgVwZW9BHKaTaw==" crossorigin="anonymous"></script>
 		<script type="text/javascript" data-cfasync="false" src="js/purify-1.0.11.js" integrity="sha512-p7UyJuyBkhMcMgE4mDsgK0Lz70OvetLefua1oXs1OujWv9gOxh4xy8InFux7bZ4/DAZsTmO4rgVwZW9BHKaTaw==" crossorigin="anonymous"></script>
-		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-tQGC7Jwx3TVRu69OGEmWl0K6lwDmgh5of3xU1Xbo4U0EY1Sv9qyUMQaOgGnMET3WMu9kbuZHsPnf7d9tTXhbsQ==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-o8Q/t6/gpmx6bQaHw3gru3cjOD5BLE/KdBKja73SllZo0/FuLvAjJ+40KhZ8ig/EpioP04etJtfTnNzF/isXow==" crossorigin="anonymous"></script>
 		<!--[if IE]>
 		<!--[if IE]>
 		<style type="text/css">body {padding-left:60px;padding-right:60px;} #ienotice {display:block;}</style>
 		<style type="text/css">body {padding-left:60px;padding-right:60px;} #ienotice {display:block;}</style>
 		<![endif]-->
 		<![endif]-->

+ 2 - 2
tst/ControllerTest.php

@@ -814,7 +814,7 @@ class ControllerTest extends PHPUnit_Framework_TestCase
     public function testReadBurnAfterReadingWithToken()
     public function testReadBurnAfterReadingWithToken()
     {
     {
         $token = base64_encode(hash_hmac(
         $token = base64_encode(hash_hmac(
-            'sha256', Helper::getPasteId(), random_bytes(32)
+            'sha256', hex2bin(Helper::getPasteId()), random_bytes(32), true
         ));
         ));
         $burnPaste = Helper::getPaste(2, array('challenge' => $token));
         $burnPaste = Helper::getPaste(2, array('challenge' => $token));
         $burnPaste['adata'][3] = 1;
         $burnPaste['adata'][3] = 1;
@@ -839,7 +839,7 @@ class ControllerTest extends PHPUnit_Framework_TestCase
     public function testReadBurnAfterReadingWithIncorrectToken()
     public function testReadBurnAfterReadingWithIncorrectToken()
     {
     {
         $token = base64_encode(hash_hmac(
         $token = base64_encode(hash_hmac(
-            'sha256', Helper::getPasteId(), random_bytes(32)
+            'sha256', hex2bin(Helper::getPasteId()), random_bytes(32), true
         ));
         ));
         $burnPaste = Helper::getPaste(2, array('challenge' => base64_encode(random_bytes(32))));
         $burnPaste = Helper::getPaste(2, array('challenge' => base64_encode(random_bytes(32))));
         $burnPaste['adata'][3] = 1;
         $burnPaste['adata'][3] = 1;

+ 3 - 1
tst/FormatV2Test.php

@@ -71,6 +71,8 @@ class FormatV2Test extends PHPUnit_Framework_TestCase
         $paste['adata'][0][7] = '!#@';
         $paste['adata'][0][7] = '!#@';
         $this->assertFalse(FormatV2::isValid($paste), 'invalid compression');
         $this->assertFalse(FormatV2::isValid($paste), 'invalid compression');
 
 
-        $this->assertFalse(FormatV2::isValid(Helper::getPaste()), 'invalid meta key');
+        $paste = Helper::getPastePost();
+        unset($paste['meta']['expire']);
+        $this->assertFalse(FormatV2::isValid($paste), 'invalid missing meta key');
     }
     }
 }
 }

+ 3 - 3
tst/ModelTest.php

@@ -276,9 +276,9 @@ class ModelTest extends PHPUnit_Framework_TestCase
     {
     {
         $pasteData = Helper::getPastePost();
         $pasteData = Helper::getPastePost();
         $pasteData['meta']['challenge'] = base64_encode(random_bytes(32));
         $pasteData['meta']['challenge'] = base64_encode(random_bytes(32));
-        $token = hash_hmac(
-            'sha256', Helper::getPasteId(), base64_decode($pasteData['meta']['challenge'])
-        );
+        $token = base64_encode(hash_hmac(
+            'sha256', hex2bin(Helper::getPasteId()), base64_decode($pasteData['meta']['challenge']), true
+        ));
         $this->_model->getPaste(Helper::getPasteId())->delete();
         $this->_model->getPaste(Helper::getPasteId())->delete();
         $paste = $this->_model->getPaste(Helper::getPasteId());
         $paste = $this->_model->getPaste(Helper::getPasteId());
         $this->assertFalse($paste->exists(), 'paste does not yet exist');
         $this->assertFalse($paste->exists(), 'paste does not yet exist');