*
* 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 "";
}
}
?>