client side token creation, handle display and single password retry

This commit is contained in:
El RIDO 2019-06-29 10:49:44 +02:00
parent 79db7ddafc
commit 5651c0f04e
No known key found for this signature in database
GPG key ID: 0F5C940A6BD81F92
9 changed files with 128 additions and 61 deletions

View file

@ -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 // might be a missing password, try one more time after getting one
Alert.showError(ServerInteraction.parseUploadError(status, data, 'get paste data')); 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(); PasteDecrypter.run(new Paste(data));
ServerInteraction.setUrl(Helper.baseUri() + '?pasteid=' + Model.getPasteId());
ServerInteraction.setFailure(function (status, data) { // restore position
// revert loading status… window.scrollTo(0, orgPosition);
Alert.hideLoading();
TopNav.showViewButtons();
// show error message // NOTE: could create problems as callback may be called
Alert.showError( // asyncronously if PasteDecrypter e.g. needs to wait for a
ServerInteraction.parseUploadError(status, data, 'refresh display') // password being entered
); callback();
});
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();
}, false); // this false is important as it circumvents the cache }, false); // this false is important as it circumvents the cache
} }

View file

@ -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

View file

@ -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']) || !array_key_exists('expire', $message['meta'])
count($message['meta']) > 1
)) { )) {
return false; return false;
} }

View file

@ -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( $this->_data['meta']['challenge'] = base64_encode(hash_hmac(
'sha256', $this->getId(), base64_decode($this->_data['meta']['challenge']) 'sha256', hex2bin($this->getId()), base64_decode($this->_data['meta']['challenge']), true
); ));
} }
// store paste // store paste

View file

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

View file

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

View file

@ -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;

View file

@ -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');
} }
} }

View file

@ -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( $token = base64_encode(hash_hmac(
'sha256', Helper::getPasteId(), base64_decode($pasteData['meta']['challenge']) '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');