client side token creation, handle display and single password retry
This commit is contained in:
parent
79db7ddafc
commit
5651c0f04e
9 changed files with 128 additions and 61 deletions
161
js/privatebin.js
161
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
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]-->
|
||||||
|
|
|
@ -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]-->
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
|
|
Loading…
Reference in a new issue