public function testSjclValidatorValidatesCorrectly() { $this->assertTrue(sjcl::isValid('{"iv":"83Ax/OdUav3SanDW9dcQPg","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gx1vA2/gQ3U","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'valid sjcl'); $this->assertFalse(sjcl::isValid('{"iv":"$","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gx1vA2/gQ3U","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'invalid base64 encoding of iv'); $this->assertFalse(sjcl::isValid('{"iv":"83Ax/OdUav3SanDW9dcQPg","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"$","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'invalid base64 encoding of salt'); $this->assertFalse(sjcl::isValid('{"iv":"83Ax/OdUav3SanDW9dcQPg","salt":"Gx1vA2/gQ3U","ct":"$"}'), 'invalid base64 encoding of ct'); $this->assertFalse(sjcl::isValid('{"iv":"MTIzNDU2Nzg5MDEyMzQ1Njc4OTA=","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gx1vA2/gQ3U","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'iv to long'); $this->assertFalse(sjcl::isValid('{"iv":"83Ax/OdUav3SanDW9dcQPg","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"MTIzNDU2Nzg5MDEyMzQ1Njc4OTA=","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'salt to long'); $this->assertFalse(sjcl::isValid('{"iv":"83Ax/OdUav3SanDW9dcQPg","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gx1vA2/gQ3U","ct":"j7ImByuE5xCqD2YXm6aSyA","foo":"MTIzNDU2Nzg5MDEyMzQ1Njc4OTA="}'), 'invalid additional key'); $this->assertFalse(sjcl::isValid('{"iv":"83Ax/OdUav3SanDW9dcQPg","v":0.9,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gx1vA2/gQ3U","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'unsupported version'); $this->assertFalse(sjcl::isValid('{"iv":"83Ax/OdUav3SanDW9dcQPg","v":1,"iter":100,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gx1vA2/gQ3U","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'not enough iterations'); $this->assertFalse(sjcl::isValid('{"iv":"83Ax/OdUav3SanDW9dcQPg","v":1,"iter":1000,"ks":127,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gx1vA2/gQ3U","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'invalid key size'); $this->assertFalse(sjcl::isValid('{"iv":"83Ax/OdUav3SanDW9dcQPg","v":1,"iter":1000,"ks":128,"ts":63,"mode":"ccm","adata":"","cipher":"aes","salt":"Gx1vA2/gQ3U","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'invalid tag length'); $this->assertFalse(sjcl::isValid('{"iv":"83Ax/OdUav3SanDW9dcQPg","v":1,"iter":1000,"ks":128,"ts":64,"mode":"!#@","adata":"","cipher":"aes","salt":"Gx1vA2/gQ3U","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'invalid mode'); $this->assertFalse(sjcl::isValid('{"iv":"83Ax/OdUav3SanDW9dcQPg","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"!#@","salt":"Gx1vA2/gQ3U","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'invalid cipher'); // @note adata is not validated, except as part of the total message length }
/** * Store new paste or comment * * POST contains: * data (mandatory) = json encoded SJCL encrypted text (containing keys: iv,salt,ct) * * All optional data will go to meta information: * expire (optional) = expiration delay (never,5min,10min,1hour,1day,1week,1month,1year,burn) (default:never) * opendiscusssion (optional) = is the discussion allowed on this paste ? (0/1) (default:0) * nickname (optional) = in discussion, encoded SJCL encrypted text nickname of author of comment (containing keys: iv,salt,ct) * parentid (optional) = in discussion, which comment this comment replies to. * pasteid (optional) = in discussion, which paste this comment belongs to. * * @access private * @param string $data * @return void */ private function _create($data) { header('Content-type: application/json'); $error = false; // Make sure last paste from the IP address was more than X seconds ago. trafficlimiter::setLimit($this->_conf['traffic']['limit']); trafficlimiter::setPath($this->_conf['traffic']['dir']); if (!trafficlimiter::canPass($_SERVER['REMOTE_ADDR'])) { $this->_return_message(1, 'Please wait ' . $this->_conf['traffic']['limit'] . ' seconds between each post.'); } // Make sure content is not too big. $sizelimit = (int) $this->_getMainConfig('sizelimit', 2097152); if (strlen($data) > $sizelimit) { $this->_return_message(1, 'Paste is limited to ' . filter::size_humanreadable($sizelimit) . ' of encrypted data.'); } // Make sure format is correct. if (!sjcl::isValid($data)) { $this->_return_message(1, 'Invalid data.'); } // Read additional meta-information. $meta = array(); // Read expiration date if (!empty($_POST['expire'])) { $selected_expire = (string) $_POST['expire']; if (array_key_exists($selected_expire, $this->_conf['expire_options'])) { $expire = $this->_conf['expire_options'][$selected_expire]; } else { $expire = $this->_conf['expire_options'][$this->_conf['expire']['default']]; } if ($expire > 0) { $meta['expire_date'] = time() + $expire; } } // Destroy the paste when it is read. if (!empty($_POST['burnafterreading'])) { $burnafterreading = $_POST['burnafterreading']; if ($burnafterreading !== '0') { if ($burnafterreading !== '1') { $error = true; } $meta['burnafterreading'] = true; } } // Read open discussion flag. if ($this->_conf['main']['opendiscussion'] && !empty($_POST['opendiscussion'])) { $opendiscussion = $_POST['opendiscussion']; if ($opendiscussion !== '0') { if ($opendiscussion !== '1') { $error = true; } $meta['opendiscussion'] = true; } } // You can't have an open discussion on a "Burn after reading" paste: if (isset($meta['burnafterreading'])) { unset($meta['opendiscussion']); } // Optional nickname for comments if (!empty($_POST['nickname'])) { // Generation of the anonymous avatar (Vizhash): // If a nickname is provided, we generate a Vizhash. // (We assume that if the user did not enter a nickname, he/she wants // to be anonymous and we will not generate the vizhash.) $nick = $_POST['nickname']; if (!sjcl::isValid($nick)) { $error = true; } else { $meta['nickname'] = $nick; $vz = new vizhash16x16(); $pngdata = $vz->generate($_SERVER['REMOTE_ADDR']); if ($pngdata != '') { $meta['vizhash'] = 'data:image/png;base64,' . base64_encode($pngdata); } // Once the avatar is generated, we do not keep the IP address, nor its hash. } } if ($error) { $this->_return_message(1, 'Invalid data.'); } // Add post date to meta. $meta['postdate'] = time(); // We just want a small hash to avoid collisions: // Half-MD5 (64 bits) will do the trick $dataid = substr(hash('md5', $data), 0, 16); $storage = array('data' => $data); // Add meta-information only if necessary. if (count($meta)) { $storage['meta'] = $meta; } // The user posts a comment. if (!empty($_POST['parentid']) && !empty($_POST['pasteid'])) { $pasteid = (string) $_POST['pasteid']; $parentid = (string) $_POST['parentid']; if (!filter::is_valid_paste_id($pasteid) || !filter::is_valid_paste_id($parentid)) { $this->_return_message(1, 'Invalid data.'); } // Comments do not expire (it's the paste that expires) unset($storage['expire_date']); unset($storage['opendiscussion']); // Make sure paste exists. if (!$this->_model()->exists($pasteid)) { $this->_return_message(1, 'Invalid data.'); } // Make sure the discussion is opened in this paste. $paste = $this->_model()->read($pasteid); if (!$paste->meta->opendiscussion) { $this->_return_message(1, 'Invalid data.'); } // Check for improbable collision. if ($this->_model()->existsComment($pasteid, $parentid, $dataid)) { $this->_return_message(1, 'You are unlucky. Try again.'); } // New comment if ($this->_model()->createComment($pasteid, $parentid, $dataid, $storage) === false) { $this->_return_message(1, 'Error saving comment. Sorry.'); } // 0 = no error $this->_return_message(0, $dataid); } else { // Check for improbable collision. if ($this->_model()->exists($dataid)) { $this->_return_message(1, 'You are unlucky. Try again.'); } // New paste if ($this->_model()->create($dataid, $storage) === false) { $this->_return_message(1, 'Error saving paste. Sorry.'); } // Generate the "delete" token. // The token is the hmac of the pasteid signed with the server salt. // The paste can be delete by calling http://myserver.com/zerobin/?pasteid=<pasteid>&deletetoken=<deletetoken> $deletetoken = hash_hmac('sha1', $dataid, serversalt::get()); // 0 = no error $this->_return_message(0, $dataid, array('deletetoken' => $deletetoken)); } $this->_return_message(1, 'Server error.'); }
/** * Set data and recalculate ID. * * @access public * @param string $data * @throws Exception * @return void */ public function setData($data) { if (!sjcl::isValid($data)) { throw new Exception('Invalid data.', 61); } $this->_data->data = $data; // We just want a small hash to avoid collisions: // Half-MD5 (64 bits) will do the trick $this->setId(substr(hash('md5', $data), 0, 16)); }
/** * Set nickname. * * @access public * @param string $nickname * @throws Exception * @return void */ public function setNickname($nickname) { if (!sjcl::isValid($nickname)) { throw new Exception('Invalid data.', 66); } $this->_data->meta->nickname = $nickname; // Generation of the anonymous avatar (Vizhash): // If a nickname is provided, we generate a Vizhash. // (We assume that if the user did not enter a nickname, he/she wants // to be anonymous and we will not generate the vizhash.) $vh = new vizhash16x16(); $pngdata = $vh->generate(trafficlimiter::getIp()); if ($pngdata != '') { $this->_data->meta->vizhash = 'data:image/png;base64,' . base64_encode($pngdata); } // Once the avatar is generated, we do not keep the IP address, nor its hash. }
/** * Set paste's attachment name. * * @access public * @param string $attachmentname * @throws Exception * @return void */ public function setAttachmentName($attachmentname) { if (!$this->_conf->getKey('fileupload') || !sjcl::isValid($attachmentname)) { throw new Exception('Invalid attachment.', 72); } $this->_data->meta->attachmentname = $attachmentname; }