diff --git a/js/privatebin.js b/js/privatebin.js
index 672d9322..e930845f 100644
--- a/js/privatebin.js
+++ b/js/privatebin.js
@@ -664,6 +664,23 @@ jQuery.PrivateBin = (function($, RawDeflate) {
*/
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
*
@@ -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
*
@@ -933,25 +977,47 @@ jQuery.PrivateBin = (function($, RawDeflate) {
* @function
* @param {string} key
* @param {string} password
- * @return {string} decrypted message, empty if decryption failed
+ * @return {string} derived key
*/
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(
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
* @return string
*/
- me.getPasteData = function(callback, useCache)
+ me.getPasteData = async function(callback, useCache)
{
// use cache if possible/allowed
if (useCache !== false && pasteData !== null) {
@@ -1173,17 +1239,31 @@ jQuery.PrivateBin = (function($, RawDeflate) {
return pasteData;
}
- // reload data
+ // load data
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) {
// revert loading status…
Alert.hideLoading();
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) {
pasteData = new Paste(data);
@@ -1909,8 +1989,9 @@ jQuery.PrivateBin = (function($, RawDeflate) {
*
* @name Prompt.requestPassword
* @function
+ * @param {function} callback
*/
- me.requestPassword = function()
+ me.requestPassword = function(callback)
{
// show new bootstrap method (if available)
if ($passwordModal.length !== 0) {
@@ -1928,9 +2009,9 @@ jQuery.PrivateBin = (function($, RawDeflate) {
}
if (password.length === 0) {
// recurse…
- return me.requestPassword();
+ return me.requestPassword(callback);
}
- PasteDecrypter.run();
+ callback();
};
/**
@@ -4087,7 +4168,7 @@ jQuery.PrivateBin = (function($, RawDeflate) {
// show notification
const baseUri = Helper.baseUri() + '?',
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);
// show new URL in browser bar
@@ -4254,7 +4335,7 @@ jQuery.PrivateBin = (function($, RawDeflate) {
]);
ServerInteraction.setUnencryptedData('meta', {
'expire': TopNav.getExpiration(),
- 'challenge': CryptTool.getCredentials(key, password)
+ 'challenge': await CryptTool.getCredentials(key, password)
});
// prepare PasteViewer for later preview
@@ -4318,7 +4399,7 @@ jQuery.PrivateBin = (function($, RawDeflate) {
// if it fails, request password
if (plaindata.length === 0 && password.length === 0) {
// show prompt
- Prompt.requestPassword();
+ Prompt.requestPassword(me.run);
// Thus, we cannot do anything yet, we need to wait for the user
// input.
@@ -4764,31 +4845,15 @@ jQuery.PrivateBin = (function($, RawDeflate) {
const orgPosition = $(window).scrollTop();
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
}
diff --git a/lib/Filter.php b/lib/Filter.php
index d7090bb4..d4840f31 100644
--- a/lib/Filter.php
+++ b/lib/Filter.php
@@ -72,6 +72,7 @@ class Filter
/**
* fixed time string comparison operation to prevent timing attacks
* https://crackstation.net/hashing-security.htm?=rd#slowequals
+ * can be replaced with hash_equals() after we drop PHP 5.5 support
*
* @access public
* @static
diff --git a/lib/FormatV2.php b/lib/FormatV2.php
index ab5ff2a7..42e0aac8 100644
--- a/lib/FormatV2.php
+++ b/lib/FormatV2.php
@@ -123,8 +123,7 @@ class FormatV2
// require only the key 'expire' in the metadata of pastes
if (!$isComment && (
count($message['meta']) === 0 ||
- !array_key_exists('expire', $message['meta']) ||
- count($message['meta']) > 1
+ !array_key_exists('expire', $message['meta'])
)) {
return false;
}
diff --git a/lib/Model/Paste.php b/lib/Model/Paste.php
index e8504f2d..88ab41d9 100644
--- a/lib/Model/Paste.php
+++ b/lib/Model/Paste.php
@@ -116,9 +116,9 @@ class Paste extends AbstractModel
$this->_data['meta']['salt'] = serversalt::generate();
// if a challenge was sent, we store the HMAC of paste ID & challenge
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
diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php
index d0ef8126..d336e791 100644
--- a/tpl/bootstrap.php
+++ b/tpl/bootstrap.php
@@ -71,7 +71,7 @@ if ($MARKDOWN):
endif;
?>
-
+
diff --git a/tpl/page.php b/tpl/page.php
index f4e1fde5..e70a319c 100644
--- a/tpl/page.php
+++ b/tpl/page.php
@@ -49,7 +49,7 @@ if ($MARKDOWN):
endif;
?>
-
+
diff --git a/tst/ControllerTest.php b/tst/ControllerTest.php
index 4c9a19dc..87d0e893 100644
--- a/tst/ControllerTest.php
+++ b/tst/ControllerTest.php
@@ -814,7 +814,7 @@ class ControllerTest extends PHPUnit_Framework_TestCase
public function testReadBurnAfterReadingWithToken()
{
$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['adata'][3] = 1;
@@ -839,7 +839,7 @@ class ControllerTest extends PHPUnit_Framework_TestCase
public function testReadBurnAfterReadingWithIncorrectToken()
{
$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['adata'][3] = 1;
diff --git a/tst/FormatV2Test.php b/tst/FormatV2Test.php
index 53473457..e6246e4c 100644
--- a/tst/FormatV2Test.php
+++ b/tst/FormatV2Test.php
@@ -71,6 +71,8 @@ class FormatV2Test extends PHPUnit_Framework_TestCase
$paste['adata'][0][7] = '!#@';
$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');
}
}
diff --git a/tst/ModelTest.php b/tst/ModelTest.php
index c7d3fa9d..8a7a581f 100644
--- a/tst/ModelTest.php
+++ b/tst/ModelTest.php
@@ -276,9 +276,9 @@ class ModelTest extends PHPUnit_Framework_TestCase
{
$pasteData = Helper::getPastePost();
$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();
$paste = $this->_model->getPaste(Helper::getPasteId());
$this->assertFalse($paste->exists(), 'paste does not yet exist');