diff --git a/js/pastemeta.jsonld b/js/pastemeta.jsonld index 2ebeca07..3294998a 100644 --- a/js/pastemeta.jsonld +++ b/js/pastemeta.jsonld @@ -26,6 +26,9 @@ }, "time_to_live": { "@type": "pb:RemainingSeconds" + }, + "challenge": { + "@type": "pb:Challenge" } } } \ No newline at end of file diff --git a/js/privatebin.js b/js/privatebin.js index 93ea2489..cb0e7ab9 100644 --- a/js/privatebin.js +++ b/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 } diff --git a/js/test/AttachmentViewer.js b/js/test/AttachmentViewer.js index 438b2f89..f59a6ae4 100644 --- a/js/test/AttachmentViewer.js +++ b/js/test/AttachmentViewer.js @@ -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', diff --git a/js/test/CryptTool.js b/js/test/CryptTool.js index 80ea5ecb..104eb05e 100644 --- a/js/test/CryptTool.js +++ b/js/test/CryptTool.js @@ -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; diff --git a/js/test/InitialCheck.js b/js/test/InitialCheck.js index 5b0778cc..90ee251f 100644 --- a/js/test/InitialCheck.js +++ b/js/test/InitialCheck.js @@ -22,7 +22,7 @@ describe('InitialCheck', function () { '' ); $.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 () { '' ); $.PrivateBin.Alert.init(); - window.crypto = null; + window.crypto = new WebCrypto(); const result1 = $.PrivateBin.InitialCheck.init(), result2 = isSecureContext === $('#httpnotice').hasClass('hidden'); clean(); diff --git a/js/types.jsonld b/js/types.jsonld index 005b68b1..d0b1ed0b 100644 --- a/js/types.jsonld +++ b/js/types.jsonld @@ -92,6 +92,9 @@ "@type": "dp:Second", "@minimum": 1 }, + "Challenge": { + "@type": "pb:Base64" + }, "CipherParameters": { "@container": "@list", "@value": [ diff --git a/lib/Controller.php b/lib/Controller.php index 2e0588b7..07b96de3 100644 --- a/lib/Controller.php +++ b/lib/Controller.php @@ -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 { 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 358d834e..42e0aac8 100644 --- a/lib/FormatV2.php +++ b/lib/FormatV2.php @@ -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; } diff --git a/lib/Model/Paste.php b/lib/Model/Paste.php index 27890840..ed98f212 100644 --- a/lib/Model/Paste.php +++ b/lib/Model/Paste.php @@ -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. * diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 711c11ab..f6011437 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 233b6931..d70c59b3 100644 --- a/tpl/page.php +++ b/tpl/page.php @@ -49,7 +49,7 @@ if ($MARKDOWN): endif; ?> - + diff --git a/tst/Bootstrap.php b/tst/Bootstrap.php index b5393510..540dc231 100644 --- a/tst/Bootstrap.php +++ b/tst/Bootstrap.php @@ -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; } diff --git a/tst/ControllerTest.php b/tst/ControllerTest.php index b00f2ce6..868d52da 100644 --- a/tst/ControllerTest.php +++ b/tst/ControllerTest.php @@ -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 */ diff --git a/tst/FormatV2Test.php b/tst/FormatV2Test.php index 8aa4d3ca..e6246e4c 100644 --- a/tst/FormatV2Test.php +++ b/tst/FormatV2Test.php @@ -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'); } } diff --git a/tst/ModelTest.php b/tst/ModelTest.php index d5c4074f..5bdb9feb 100644 --- a/tst/ModelTest.php +++ b/tst/ModelTest.php @@ -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; diff --git a/tst/RequestTest.php b/tst/RequestTest.php index 9b440be0..b92d903d 100644 --- a/tst/RequestTest.php +++ b/tst/RequestTest.php @@ -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();