Compare commits
6 commits
master
...
challenge-
Author | SHA1 | Date | |
---|---|---|---|
|
95ed1251a3 | ||
|
74519f6c4f | ||
|
5651c0f04e | ||
|
79db7ddafc | ||
|
d0c8975b89 | ||
|
9d2d7bde42 |
17 changed files with 400 additions and 61 deletions
|
@ -26,6 +26,9 @@
|
|||
},
|
||||
"time_to_live": {
|
||||
"@type": "pb:RemainingSeconds"
|
||||
},
|
||||
"challenge": {
|
||||
"@type": "pb:Challenge"
|
||||
}
|
||||
}
|
||||
}
|
157
js/privatebin.js
157
js/privatebin.js
|
@ -851,10 +851,12 @@ jQuery.PrivateBin = (function($, RawDeflate) {
|
|||
* @param {string} key
|
||||
* @param {string} password
|
||||
* @param {array} spec cryptographic specification
|
||||
* @param {bool} exportKey
|
||||
* @return {CryptoKey} derived key
|
||||
*/
|
||||
async function deriveKey(key, password, spec)
|
||||
async function deriveKey(key, password, spec, exportKey)
|
||||
{
|
||||
exportKey = exportKey || false;
|
||||
let keyArray = stringToArraybuffer(key);
|
||||
if (password.length > 0) {
|
||||
// version 1 pastes did append the passwords SHA-256 hash in hex
|
||||
|
@ -899,11 +901,38 @@ jQuery.PrivateBin = (function($, RawDeflate) {
|
|||
name: 'AES-' + spec[6].toUpperCase(), // can be any supported AES algorithm ("AES-CTR", "AES-CBC", "AES-CMAC", "AES-GCM", "AES-CFB", "AES-KW", "ECDH", "DH" or "HMAC")
|
||||
length: spec[3] // can be 128, 192 or 256
|
||||
},
|
||||
false, // the key may not be exported
|
||||
exportKey, // may the key get exported, false by default
|
||||
['encrypt', 'decrypt'] // we may only use it for en- and decryption
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
|
@ -924,6 +953,56 @@ jQuery.PrivateBin = (function($, RawDeflate) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* get PBKDF2 protected credentials for server to validate password
|
||||
*
|
||||
* @name CryptTool.getCredentials
|
||||
* @function
|
||||
* @param {string} key
|
||||
* @param {string} password
|
||||
* @return {string} derived key
|
||||
*/
|
||||
me.getCredentials = async function(key, password)
|
||||
{
|
||||
return btoa(
|
||||
arraybufferToString(
|
||||
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(id)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* compress, then encrypt message with given key and password
|
||||
*
|
||||
|
@ -1128,7 +1207,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) {
|
||||
|
@ -1141,17 +1220,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);
|
||||
|
@ -1877,8 +1970,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) {
|
||||
|
@ -1896,9 +1990,9 @@ jQuery.PrivateBin = (function($, RawDeflate) {
|
|||
}
|
||||
if (password.length === 0) {
|
||||
// recurse…
|
||||
return me.requestPassword();
|
||||
return me.requestPassword(callback);
|
||||
}
|
||||
PasteDecrypter.run();
|
||||
callback();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -4055,7 +4149,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
|
||||
|
@ -4197,7 +4291,9 @@ jQuery.PrivateBin = (function($, RawDeflate) {
|
|||
|
||||
// prepare server interaction
|
||||
ServerInteraction.prepare();
|
||||
ServerInteraction.setCryptParameters(TopNav.getPassword());
|
||||
const key = CryptTool.getSymmetricKey(),
|
||||
password = TopNav.getPassword();
|
||||
ServerInteraction.setCryptParameters(password, key);
|
||||
|
||||
// set success/fail functions
|
||||
ServerInteraction.setSuccess(showCreatedPaste);
|
||||
|
@ -4218,7 +4314,10 @@ jQuery.PrivateBin = (function($, RawDeflate) {
|
|||
TopNav.getOpenDiscussion() ? 1 : 0,
|
||||
TopNav.getBurnAfterReading() ? 1 : 0
|
||||
]);
|
||||
ServerInteraction.setUnencryptedData('meta', {'expire': TopNav.getExpiration()});
|
||||
ServerInteraction.setUnencryptedData('meta', {
|
||||
'expire': TopNav.getExpiration(),
|
||||
'challenge': await CryptTool.getCredentials(key, password)
|
||||
});
|
||||
|
||||
// prepare PasteViewer for later preview
|
||||
PasteViewer.setText(plainText);
|
||||
|
@ -4281,7 +4380,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.
|
||||
|
@ -4727,31 +4826,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
|
||||
}
|
||||
|
||||
|
|
|
@ -4,9 +4,6 @@ var common = require('../common');
|
|||
describe('AttachmentViewer', function () {
|
||||
describe('setAttachment, showAttachment, removeAttachment, hideAttachment, hideAttachmentPreview, hasAttachment, getAttachment & moveAttachmentTo', function () {
|
||||
this.timeout(30000);
|
||||
before(function () {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
jsc.property(
|
||||
'displays & hides data as requested',
|
||||
|
|
|
@ -237,19 +237,48 @@ conseq_or_bottom inv (interp (nth_iterate sBody n) (MemElem mem))
|
|||
});
|
||||
});
|
||||
|
||||
describe('getCredentials', function () {
|
||||
it('generates credentials with password', async function () {
|
||||
const clean = jsdom();
|
||||
window.crypto = new WebCrypto();
|
||||
// choosen by fair dice roll
|
||||
const key = atob('EqueAutxlrekNNEvJWB1uaaiwbk/GGpn4++cdk+uDMc='),
|
||||
// -- "That's amazing. I've got the same combination on my luggage."
|
||||
password = Array.apply(0, Array(6)).map((_,b) => b + 1).join('');
|
||||
const credentials = await $.PrivateBin.CryptTool.getCredentials(
|
||||
key, password
|
||||
);
|
||||
clean();
|
||||
assert.strictEqual(credentials, 'JS8bJWFx1bAPI2LMxfWrw4AQ7cedNVl8UmjUd/pW7Yg=');
|
||||
});
|
||||
|
||||
it('generates credentials without password', async function () {
|
||||
const clean = jsdom();
|
||||
window.crypto = new WebCrypto();
|
||||
// choosen by fair dice roll
|
||||
const key = atob('U844LK1y2uUPthTgMvPECwGyQzwScCwkaEI/+qLfQSE='),
|
||||
password = '';
|
||||
const credentials = await $.PrivateBin.CryptTool.getCredentials(
|
||||
key, password
|
||||
);
|
||||
clean();
|
||||
assert.strictEqual(credentials, 'VfAvY7T9rm3K3JKtiOeb+B+rXnE6yZ4bYQTaD9jwjEk=');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSymmetricKey', function () {
|
||||
this.timeout(30000);
|
||||
var keys = [];
|
||||
let keys = [];
|
||||
|
||||
// the parameter is used to ensure the test is run more then one time
|
||||
jsc.property(
|
||||
'returns random, non-empty keys',
|
||||
'integer',
|
||||
function(counter) {
|
||||
var clean = jsdom();
|
||||
const clean = jsdom();
|
||||
window.crypto = new WebCrypto();
|
||||
var key = $.PrivateBin.CryptTool.getSymmetricKey(),
|
||||
result = (key !== '' && keys.indexOf(key) === -1);
|
||||
const key = $.PrivateBin.CryptTool.getSymmetricKey(),
|
||||
result = (key !== '' && keys.indexOf(key) === -1);
|
||||
keys.push(key);
|
||||
clean();
|
||||
return result;
|
||||
|
|
|
@ -22,7 +22,7 @@ describe('InitialCheck', function () {
|
|||
'</body></html>'
|
||||
);
|
||||
$.PrivateBin.Alert.init();
|
||||
window.crypto = null;
|
||||
window.crypto = new WebCrypto();
|
||||
const result1 = !$.PrivateBin.InitialCheck.init(),
|
||||
result2 = !$('#errormessage').hasClass('hidden');
|
||||
clean();
|
||||
|
@ -76,7 +76,7 @@ describe('InitialCheck', function () {
|
|||
'</body></html>'
|
||||
);
|
||||
$.PrivateBin.Alert.init();
|
||||
window.crypto = null;
|
||||
window.crypto = new WebCrypto();
|
||||
const result1 = $.PrivateBin.InitialCheck.init(),
|
||||
result2 = isSecureContext === $('#httpnotice').hasClass('hidden');
|
||||
clean();
|
||||
|
|
|
@ -92,6 +92,9 @@
|
|||
"@type": "dp:Second",
|
||||
"@minimum": 1
|
||||
},
|
||||
"Challenge": {
|
||||
"@type": "pb:Base64"
|
||||
},
|
||||
"CipherParameters": {
|
||||
"@container": "@list",
|
||||
"@value": [
|
||||
|
|
|
@ -276,9 +276,7 @@ class Controller
|
|||
// accessing this method ensures that the paste would be
|
||||
// deleted if it has already expired
|
||||
$paste->get();
|
||||
if (
|
||||
Filter::slowEquals($deletetoken, $paste->getDeleteToken())
|
||||
) {
|
||||
if ($paste->isDeleteTokenCorrect($deletetoken)) {
|
||||
// Paste exists and deletion token is valid: Delete the paste.
|
||||
$paste->delete();
|
||||
$this->_status = 'Paste was properly deleted.';
|
||||
|
@ -315,9 +313,20 @@ class Controller
|
|||
try {
|
||||
$paste = $this->_model->getPaste($dataid);
|
||||
if ($paste->exists()) {
|
||||
// handle challenge response
|
||||
if (!$paste->isTokenCorrect($this->_request->getParam('token'))) {
|
||||
// we send a generic error to avoid leaking information
|
||||
// about the existance of a burn after reading pastes
|
||||
// this avoids an attacker being able to poll, if it has
|
||||
// been read by the intended recipient or not
|
||||
$this->_return_message(1, self::GENERIC_ERROR);
|
||||
return;
|
||||
}
|
||||
$data = $paste->get();
|
||||
if (array_key_exists('salt', $data['meta'])) {
|
||||
unset($data['meta']['salt']);
|
||||
foreach (array('salt', 'challenge') as $key) {
|
||||
if (array_key_exists($key, $data['meta'])) {
|
||||
unset($data['meta'][$key]);
|
||||
}
|
||||
}
|
||||
$this->_return_message(0, $dataid, (array) $data);
|
||||
} else {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -67,6 +67,13 @@ class FormatV2
|
|||
if (!($ct = base64_decode($message['ct'], true))) {
|
||||
return false;
|
||||
}
|
||||
// - (optional) challenge
|
||||
if (
|
||||
!$isComment && array_key_exists('challenge', $message['meta']) &&
|
||||
!base64_decode($message['meta']['challenge'], true)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure some fields have a reasonable size:
|
||||
// - initialization vector
|
||||
|
@ -116,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;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ namespace PrivateBin\Model;
|
|||
|
||||
use Exception;
|
||||
use PrivateBin\Controller;
|
||||
use PrivateBin\Filter;
|
||||
use PrivateBin\Persistence\ServerSalt;
|
||||
|
||||
/**
|
||||
|
@ -23,6 +24,14 @@ use PrivateBin\Persistence\ServerSalt;
|
|||
*/
|
||||
class Paste extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* Token for challenge/response.
|
||||
*
|
||||
* @access protected
|
||||
* @var string
|
||||
*/
|
||||
protected $_token = '';
|
||||
|
||||
/**
|
||||
* Get paste data.
|
||||
*
|
||||
|
@ -32,6 +41,11 @@ class Paste extends AbstractModel
|
|||
*/
|
||||
public function get()
|
||||
{
|
||||
// return cached result if one is found
|
||||
if (array_key_exists('adata', $this->_data) || array_key_exists('data', $this->_data)) {
|
||||
return $this->_data;
|
||||
}
|
||||
|
||||
$data = $this->_store->read($this->getId());
|
||||
if ($data === false) {
|
||||
throw new Exception(Controller::GENERIC_ERROR, 64);
|
||||
|
@ -48,10 +62,16 @@ class Paste extends AbstractModel
|
|||
unset($data['meta']['expire_date']);
|
||||
}
|
||||
|
||||
// check if non-expired burn after reading paste needs to be deleted
|
||||
// check if non-expired burn after reading paste needs to be deleted,
|
||||
// but don't delete it if an incorrect token was sent
|
||||
if (
|
||||
(array_key_exists('adata', $data) && $data['adata'][3] === 1) ||
|
||||
(array_key_exists('burnafterreading', $data['meta']) && $data['meta']['burnafterreading'])
|
||||
(
|
||||
(array_key_exists('adata', $data) && $data['adata'][3] === 1) ||
|
||||
(array_key_exists('burnafterreading', $data['meta']) && $data['meta']['burnafterreading'])
|
||||
) && (
|
||||
!array_key_exists('challenge', $data['meta']) ||
|
||||
$this->_token === $data['meta']['challenge']
|
||||
)
|
||||
) {
|
||||
$this->delete();
|
||||
}
|
||||
|
@ -94,6 +114,12 @@ class Paste extends AbstractModel
|
|||
|
||||
$this->_data['meta']['created'] = time();
|
||||
$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'] = base64_encode(hash_hmac(
|
||||
'sha256', $this->getId(), base64_decode($this->_data['meta']['challenge']), true
|
||||
));
|
||||
}
|
||||
|
||||
// store paste
|
||||
if (
|
||||
|
@ -201,6 +227,40 @@ class Paste extends AbstractModel
|
|||
(array_key_exists('opendiscussion', $this->_data['meta']) && $this->_data['meta']['opendiscussion']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if paste challenge matches provided token.
|
||||
*
|
||||
* @access public
|
||||
* @param string $token
|
||||
* @throws Exception
|
||||
* @return bool
|
||||
*/
|
||||
public function isTokenCorrect($token)
|
||||
{
|
||||
$this->_token = $token;
|
||||
if (!array_key_exists('challenge', $this->_data['meta'])) {
|
||||
$this->get();
|
||||
}
|
||||
if (array_key_exists('challenge', $this->_data['meta'])) {
|
||||
return Filter::slowEquals($token, $this->_data['meta']['challenge']);
|
||||
}
|
||||
// paste created without challenge, accept every token sent
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if paste salt based HMAC matches provided delete token.
|
||||
*
|
||||
* @access public
|
||||
* @param string $deletetoken
|
||||
* @throws Exception
|
||||
* @return bool
|
||||
*/
|
||||
public function isDeleteTokenCorrect($deletetoken)
|
||||
{
|
||||
return Filter::slowEquals($deletetoken, $this->getDeleteToken());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes data to conform with current configuration.
|
||||
*
|
||||
|
|
|
@ -71,7 +71,7 @@ if ($MARKDOWN):
|
|||
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/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-Yq2HyT+H1PmQxCaDeh6E/ChOrTBSYsu8BuS4yb8UPHlyMVaxqSOtyfy6hx6vAsVT0G3bKeLRAuejhvPTOoz7fQ==" crossorigin="anonymous"></script>
|
||||
<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-r9MutKcgP/igbs8aUbENyJEie7LMyJ22f2On0RwGL0Hq0seJnmnPo4avDfhR0E/TZWDoux2arzxYHneH2/Ltmw==" crossorigin="anonymous"></script>
|
||||
<!--[if IE]>
|
||||
<style type="text/css">body {padding-left:60px;padding-right:60px;} #ienotice {display:block;}</style>
|
||||
<![endif]-->
|
||||
|
|
|
@ -49,7 +49,7 @@ if ($MARKDOWN):
|
|||
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/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-Yq2HyT+H1PmQxCaDeh6E/ChOrTBSYsu8BuS4yb8UPHlyMVaxqSOtyfy6hx6vAsVT0G3bKeLRAuejhvPTOoz7fQ==" crossorigin="anonymous"></script>
|
||||
<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-r9MutKcgP/igbs8aUbENyJEie7LMyJ22f2On0RwGL0Hq0seJnmnPo4avDfhR0E/TZWDoux2arzxYHneH2/Ltmw==" crossorigin="anonymous"></script>
|
||||
<!--[if IE]>
|
||||
<style type="text/css">body {padding-left:60px;padding-right:60px;} #ienotice {display:block;}</style>
|
||||
<![endif]-->
|
||||
|
|
|
@ -155,7 +155,7 @@ class Helper
|
|||
public static function getPastePost($version = 2, array $meta = array())
|
||||
{
|
||||
$example = self::getPaste($version, $meta);
|
||||
$example['meta'] = array('expire' => $example['meta']['expire']);
|
||||
$example['meta'] = array_merge(array('expire' => $example['meta']['expire']), $meta);
|
||||
return $example;
|
||||
}
|
||||
|
||||
|
|
|
@ -366,6 +366,30 @@ class ControllerTest extends PHPUnit_Framework_TestCase
|
|||
$this->assertEquals(1, $paste['adata'][2], 'discussion is enabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
*/
|
||||
public function testCreateInvalidFormat()
|
||||
{
|
||||
$options = parse_ini_file(CONF, true);
|
||||
$options['traffic']['limit'] = 0;
|
||||
Helper::createIniFile(CONF, $options);
|
||||
$paste = Helper::getPasteJson(2, array('challenge' => '$'));
|
||||
$file = tempnam(sys_get_temp_dir(), 'FOO');
|
||||
file_put_contents($file, $paste);
|
||||
Request::setInputStream($file);
|
||||
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
$_SERVER['REMOTE_ADDR'] = '::1';
|
||||
ob_start();
|
||||
new Controller;
|
||||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$response = json_decode($content, true);
|
||||
$this->assertEquals(1, $response['status'], 'outputs error status');
|
||||
$this->assertFalse($this->_data->exists(Helper::getPasteId()), 'paste exists after posting data');
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
*/
|
||||
|
@ -784,6 +808,56 @@ class ControllerTest extends PHPUnit_Framework_TestCase
|
|||
$this->assertFalse($this->_data->exists(Helper::getPasteId()), 'paste successfully deleted');
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
*/
|
||||
public function testReadBurnAfterReadingWithToken()
|
||||
{
|
||||
$token = base64_encode(hash_hmac(
|
||||
'sha256', Helper::getPasteId(), random_bytes(32), true
|
||||
));
|
||||
$burnPaste = Helper::getPaste(2, array('challenge' => $token));
|
||||
$burnPaste['adata'][3] = 1;
|
||||
$this->_data->create(Helper::getPasteId(), $burnPaste);
|
||||
$this->assertTrue($this->_data->exists(Helper::getPasteId()), 'paste exists before deleting data');
|
||||
$_SERVER['QUERY_STRING'] = Helper::getPasteId() . '&token=' . $token;
|
||||
$_GET[Helper::getPasteId()] = '';
|
||||
$_GET['token'] = $token;
|
||||
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
|
||||
ob_start();
|
||||
new Controller;
|
||||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$response = json_decode($content, true);
|
||||
$this->assertEquals(0, $response['status'], 'outputs status');
|
||||
$this->assertFalse($this->_data->exists(Helper::getPasteId()), 'paste successfully deleted');
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
*/
|
||||
public function testReadBurnAfterReadingWithIncorrectToken()
|
||||
{
|
||||
$token = base64_encode(hash_hmac(
|
||||
'sha256', Helper::getPasteId(), random_bytes(32), true
|
||||
));
|
||||
$burnPaste = Helper::getPaste(2, array('challenge' => base64_encode(random_bytes(32))));
|
||||
$burnPaste['adata'][3] = 1;
|
||||
$this->_data->create(Helper::getPasteId(), $burnPaste);
|
||||
$this->assertTrue($this->_data->exists(Helper::getPasteId()), 'paste exists before deleting data');
|
||||
$_SERVER['QUERY_STRING'] = Helper::getPasteId() . '&token=' . $token;
|
||||
$_GET[Helper::getPasteId()] = '';
|
||||
$_GET['token'] = $token;
|
||||
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
|
||||
ob_start();
|
||||
new Controller;
|
||||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$response = json_decode($content, true);
|
||||
$this->assertEquals(1, $response['status'], 'outputs status');
|
||||
$this->assertTrue($this->_data->exists(Helper::getPasteId()), 'paste not deleted');
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
*/
|
||||
|
|
|
@ -21,6 +21,10 @@ class FormatV2Test extends PHPUnit_Framework_TestCase
|
|||
$paste['ct'] = '$';
|
||||
$this->assertFalse(FormatV2::isValid($paste), 'invalid base64 encoding of ct');
|
||||
|
||||
$paste = Helper::getPastePost();
|
||||
$paste['meta']['challenge'] = '$';
|
||||
$this->assertFalse(FormatV2::isValid($paste), 'invalid base64 encoding of ct');
|
||||
|
||||
$paste = Helper::getPastePost();
|
||||
$paste['ct'] = 'bm9kYXRhbm9kYXRhbm9kYXRhbm9kYXRhbm9kYXRhCg==';
|
||||
$this->assertFalse(FormatV2::isValid($paste), 'low ct entropy');
|
||||
|
@ -67,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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -268,10 +268,48 @@ class ModelTest extends PHPUnit_Framework_TestCase
|
|||
$paste->setData($pasteData);
|
||||
$paste->store();
|
||||
|
||||
$paste = $paste->get();
|
||||
$paste = $this->_model->getPaste(Helper::getPasteId())->get();
|
||||
$this->assertEquals((float) 300, (float) $paste['meta']['time_to_live'], 'remaining time is set correctly', 1.0);
|
||||
}
|
||||
|
||||
public function testToken()
|
||||
{
|
||||
$pasteData = Helper::getPastePost();
|
||||
$pasteData['meta']['challenge'] = base64_encode(random_bytes(32));
|
||||
$token = base64_encode(hash_hmac(
|
||||
'sha256', 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');
|
||||
|
||||
$paste = $this->_model->getPaste();
|
||||
$paste->setData($pasteData);
|
||||
$paste->store();
|
||||
|
||||
$paste = $this->_model->getPaste(Helper::getPasteId());
|
||||
$this->assertTrue(
|
||||
$paste->isTokenCorrect($token),
|
||||
'token is accepted after store and retrieval'
|
||||
);
|
||||
}
|
||||
|
||||
public function testDeleteToken()
|
||||
{
|
||||
$pasteData = Helper::getPastePost();
|
||||
$this->_model->getPaste(Helper::getPasteId())->delete();
|
||||
$paste = $this->_model->getPaste(Helper::getPasteId());
|
||||
$this->assertFalse($paste->exists(), 'paste does not yet exist');
|
||||
|
||||
$paste = $this->_model->getPaste();
|
||||
$paste->setData($pasteData);
|
||||
$paste->store();
|
||||
$deletetoken = $paste->getDeleteToken();
|
||||
|
||||
$paste = $this->_model->getPaste(Helper::getPasteId());
|
||||
$this->assertTrue($paste->isDeleteTokenCorrect($deletetoken), 'delete token is accepted after store and retrieval');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Exception
|
||||
* @expectedExceptionCode 64
|
||||
|
@ -287,6 +325,20 @@ class ModelTest extends PHPUnit_Framework_TestCase
|
|||
$paste->getComment(Helper::getPasteId())->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Exception
|
||||
* @expectedExceptionCode 75
|
||||
*/
|
||||
public function testInvalidFormat()
|
||||
{
|
||||
$pasteData = Helper::getPastePost();
|
||||
$pasteData['adata'][1] = 'foo';
|
||||
$this->_model->getPaste(Helper::getPasteId())->delete();
|
||||
|
||||
$paste = $this->_model->getPaste();
|
||||
$paste->setData($pasteData);
|
||||
}
|
||||
|
||||
public function testPurge()
|
||||
{
|
||||
$conf = new Configuration;
|
||||
|
|
|
@ -130,6 +130,22 @@ class RequestTest extends PHPUnit_Framework_TestCase
|
|||
$this->assertEquals('read', $request->getOperation());
|
||||
}
|
||||
|
||||
public function testApiReadWithToken()
|
||||
{
|
||||
$this->reset();
|
||||
$id = $this->getRandomId();
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_SERVER['HTTP_ACCEPT'] = 'application/json, text/javascript, */*; q=0.01';
|
||||
$_SERVER['QUERY_STRING'] = $id . '&token=foo';
|
||||
$_GET[$id] = '';
|
||||
$_GET['token'] = 'foo';
|
||||
$request = new Request;
|
||||
$this->assertTrue($request->isJsonApiCall(), 'is JSON Api call');
|
||||
$this->assertEquals($id, $request->getParam('pasteid'));
|
||||
$this->assertEquals('foo', $request->getParam('token'));
|
||||
$this->assertEquals('read', $request->getOperation());
|
||||
}
|
||||
|
||||
public function testApiDelete()
|
||||
{
|
||||
$this->reset();
|
||||
|
|
Loading…
Reference in a new issue