* * TODO Notes: currently get some parser errors multiple keys have to look at moving pages as that could change the encryption key! (doesn't seem to be easy to solve, just leave as is) for better security, store keys as cookies (probably not always desirable) perhaps rename (doesn't really encrypt a page) diffs don't work on plaintext only thing uncertain of is how could allow auth users to see diffs, probably should override the view action, check for diff being set and handle it by overrriding the DifferenceEngine getText function, otherwise call the defaults for these, perhaps easiest to use setText in DifferenceEngine, only available in 1.6, in 1.5 called showDiff, look in EditPage to see how it shows diff for conflicts seems to be a cache problem right after save. can't get it to not cache it problem is in article.php in updatearticle calls editUpdates which saves to cache seems to definitely be MW bug reported, being fixed on edits, check the number of encrypt tags before and after edit if decrease, have user confirm that they want to declassify text (want to make sure not make private info public) docs alt= tags for display in the non-decryptable case, default is no text algorithm= mode= Security Notes: only provides for privacy, still wiki -- anyone can edit default cipher is rijndael-128==AES and mode is cbc client side (javascript encryption) including AES http://home.versatel.nl/MAvanEverdingen/Code/ plain text never stored in database plain text keys not stored in database someone with access to database can decrypt your text if you can if you and everyone else with access deletes the encryption key from preferences, then the data will presumably be secure from then on into the future if trust unencrypted email, network security, then this is by far more security Images/file uploads however cannot be encrypted. page titles are not encrypted (could be info there) page length is visible (not padded), so there's some info there we can store all passwords in one user option the passwords are sha1 hashes in hexadecimal so we know that they don't have any bad characters we can do the same with the page names (or base64 encode them). makes it easy to 'encode' multiple things together */ $wgExtensionFunctions[] = "wfEncryptPageExtension"; $wgExtensionCredits['parserhook'][] = array( 'name' => 'EncryptPage', 'version' => '2006/04/20', 'author' => 'Austin Che', 'url' => 'http://openwetware.org/wiki/User:Austin/Extensions/EncryptPage', 'description' => 'Enable encrypting wiki pages', ); $wgEncryptPage = null; function wfEncryptPageExtension() { global $wgParser, $wgEncryptPage, $wgHooks, $wgMessageCache; $wgMessageCache->addMessages(array('prefs-encryption' => 'Encryption', 'tog-encrypt-enable' => 'Enable encryption', 'encrypt-default-key' => 'Default encryption key')); $wgEncryptPage = new EncryptPage(); // on edit/save, we check in edit filter whether user has proper key to encrypt // and on save, we replace all plaintext blocks with encrypted versions $wgHooks['EditFilter'][] = array(&$wgEncryptPage, 'processEdit'); $wgHooks['ArticleSave'][] = array(&$wgEncryptPage, 'processSave'); $wgHooks['ArticleAfterFetchContent'][] = array(&$wgEncryptPage, 'processFetch'); $wgParser->setHook("encrypt", array(&$wgEncryptPage, 'encryptTag')); $wgParser->setHook("encrypted", array(&$wgEncryptPage, 'encryptedTag')); if (function_exists("wfAddPreferences")) { wfAddPreferences(array(array('name' => "encrypt-enable", 'section' => "prefs-encryption", 'type' => PREF_TOGGLE_T), array('name' => "encrypt-default-key", 'section' => "prefs-encryption", 'type' => PREF_PASSWORD_T, 'size' => 30, 'load' => "loadPassword", 'save' => "savePassword"))); } } function savePassword($name, $value) { global $wgUser; $pass = $wgUser->getOption($name); if ($pass) $pass = md5($pass); // only set new value if it has changed if ($value != $pass) $wgUser->setOption($name, sha1($value)); // we only store hash of password } function loadPassword($name) { global $wgUser; // we load a hash of the current password as the value for the form element // so people can see if the value is set yet not retrieve the current value $pass = $wgUser->getOption($name); if ($pass) $pass = md5($pass); // md5 is shorter than sha1 and is sufficient return $pass; } class EncryptPage { var $key = null; var $num = 0; function EncryptPage() { global $wgUser; if ($wgUser->isAnon() || ! $wgUser->getOption("encrypt-enable")) return; $this->key = $wgUser->getOption("encrypt-default-key"); } function processFetch(&$article, &$text) { // To get the contents of a page for viewing or editing // decrypt it so that it is decrypted for editing // for viewing, we'll handle in the tag processing if (! $this->key) return true; $replaced = Parser::extractTagsAndParams("encrypted", $text, $content, $tags, $params); foreach ($content as $marker => $s) { $decrypt = $this->decryptText($s, $params[$marker]['algorithm'], $params[$marker]['mode']); if ($decrypt) { $content[$marker] = str_replace(""; } else { // leave encrypted if we couldn't decrypt properly $content[$marker] = $tags[$marker] . $s . ""; } } $state = array('encrypt' => $content); $text = Parser::unstrip($replaced, $state); return true; } function processSave(&$article, &$user, &$text) { $replaced = Parser::extractTagsAndParams("encrypt", $text, $content, $tags, $params); if (count($content) == 0) return true; if (! $this->key) return false; // this shouldn't happen, we should catch in processEdit foreach ($content as $marker => $s) { $content[$marker] = str_replace("encryptText($s, $params[$marker]['algorithm'], $params[$marker]['mode']) . ""; } $state = array('encrypt' => $content); $text = Parser::unstrip($replaced, $state); return true; } /** * Callback on edit attempts, check that if have encrypt blocks, we have a key. * @param EditPage $editPage * @param string $newtext * @param string $section * @param bool true to continue saving, false to abort */ function processEdit(&$editPage, $newtext, $section) { // if there are no blocks to encrypt, don't need to worry about it $replaced = Parser::extractTagsAndParams("encrypt", $text, $content, $tags, $params); if (count($content) == 0) return true; // if cannot successfully encrypt, we abort the edit if (! $this->key) { $editPage->showEditForm( array( &$this, 'editCallbackNoKey' ) ); return false; } // *** check if number of encrypt tags has been reduced and if so warn return true; } /** * Insert error message * @param OutputPage $out */ function editCallbackNoKey( &$out ) { $out->addHTML("Error: No key available for encrypting this page."); } function encryptText($text, $algorithm="rijndael-128", $mode="cbc") { if (! in_array($algorithm, mcrypt_list_algorithms())) $algorithm = "rijndael-128"; if (! in_array($mode, mcrypt_list_modes())) $mode = "cbc"; $td = mcrypt_module_open($algorithm, '', $mode, ''); // get an IV // we generate one in a fixed manner so that when re-encrypting a page // the same IV will used so that the same block will be encrypted the same // this is to ensure that unnecessary changes don't show up in the diffs for pages // we use hash of the page name and an incrementing number global $wgTitle; $iv = substr(sha1($wgTitle->getText() . $this->num++, true), 0, mcrypt_enc_get_iv_size($td)); // get the right number of bytes from our key $ks = mcrypt_enc_get_key_size($td); $key = substr($this->key, 0, $ks); // we need to rtrim to remove padding added by the decryption // to make sure the encrypted text matches the decrypted, we also trim the text // before encryption even though // the text is unlikely to contain a null character $text = rtrim($text, "\0"); // this is our encryption // prepend hash of text, encrypt that, then prepend iv, then base64 encode mcrypt_generic_init($td, $key, $iv); $encrypted = base64_encode($iv . mcrypt_generic($td, sha1($text, true) . $text)); mcrypt_generic_deinit($td); mcrypt_module_close($td); return $encrypted; } function decryptText($text, $algorithm="rijndael-128", $mode="cbc") { // returns null if text cannot be properly decrypted if (! $this->key) return null; if (! in_array($algorithm, mcrypt_list_algorithms())) $algorithm = "rijndael-128"; if (! in_array($mode, mcrypt_list_modes())) $mode = "cbc"; $td = mcrypt_module_open($algorithm, '', $mode, ''); $text = base64_decode($text); // grab the iv from the beginning $ivsize = mcrypt_enc_get_iv_size($td); if (strlen($text) < $ivsize) return null; $iv = substr($text, 0, $ivsize); $cipher = substr($text, $ivsize); // get the right number of bytes from our key $ks = mcrypt_enc_get_key_size($td); $key = substr($this->key, 0, $ks); mcrypt_generic_init($td, $key, $iv); $decrypted = rtrim(mdecrypt_generic($td, $cipher), "\0"); // remove padding with rtrim $sha1 = substr($decrypted, 0, 20); // sha1 is 20 bytes in binary format $decrypted = substr($decrypted, 20); mcrypt_generic_deinit($td); mcrypt_module_close($td); // check correct decryption if ($sha1 != sha1($decrypted, true)) return null; return $decrypted; } function encryptTag($text, $argv, &$parser) { // this is in plain-text (e.g. for edit preview or if we have key) global $wgOut; $text = $wgOut->parse($text, false); $parser->disableCache(); return $text; } function encryptedTag($text, $argv, &$parser) { // cannot cache as different people will see decrypted or encrypted version $parser->disableCache(); if ($argv['alt']) return $argv['alt']; else return ""; } } ?>