Created
September 12, 2012 02:19
-
-
Save mwgamera/3703797 to your computer and use it in GitHub Desktop.
Ultimate obfuscator for e-mail addresses
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Obfuscator | |
========== | |
Simple class that allows protecting some vulnerable data like e-mail addresses | |
from automatically harvesting it by forcing additional request. Most e-mail | |
harvesters don't even bother processing anything that doesn't look like e-mail | |
address in the first place, so any kind of obfuscation will prevent them from | |
harvesting and it's trivially achievable as long we can afford forcing | |
legitimate users to have ECMAScript enabled. | |
If, however, certain way of obfuscating becomes popular enough or attacker | |
targets concrete site, nothing prevents them from making dedicated harvester | |
that can deal with obfuscation. Therefore the scheme I propose forces attacker | |
to issue a separate request for each page of data. | |
Proposed scheme | |
--------------- | |
In the proposed scheme data is sent encrypted with XXTEA algorithm along | |
with random seed. Key used to encrypt the data is derived from the seed | |
and server secret. Separate script provides a mapping from seed to key. | |
The script that provides that mapping may contain arbitrary sleeps to make | |
harvesting attacks even less feasible (but be careful not to punish | |
legitimate users with annoying loading times!). | |
I've chosen XXTEA because of its simplicity and brevity of code required to | |
implement it in ECMAScript. The only other cipher that simple is RC4 which | |
requires more memory. The purpose of encryption is to force additional | |
request, so the cipher used doesn't need to provide military-grade security. | |
Only requirement is that breaking it must be less feasible than making | |
additional request. Using secure cipher, however, gives strong security | |
guaranties for devised scheme. | |
Key derivation function is simplistic key stretching based on SHA-1. | |
Guessing the key without knowing the server secret should be infeasible. | |
As the single seed is used for entire page and mapping from seed to key | |
is constant (as long the server secret is constant), it should not interfere | |
with caching in any way. | |
Although primary intended use is to have mailto links obfuscated, data | |
is actually arbitrary UTF-8 string. After encryption it is encoded with | |
with Base64url therefore it is safe to embed in SGML/XML, ECMAScript source | |
and URIs without additional escaping. | |
PHP API | |
------- | |
Obfuscator::__construct(string $serverSecret [, string $seed]) | |
Create an Obfuscator instance with given server secret and optionally | |
fixing the seed. If seed is not given, random will be used. | |
string Obfuscator::get_seed(void) | |
Get seed used by this Obfuscator. | |
string Obfuscator::set_seed(string $seed) | |
Set seed to be used by this Obfuscator. | |
string Obfuscator::get_key_json(void) | |
Get encryption key derived from seed as used by this Obfuscator. | |
string Obfuscator::obfuscate(string $data) | |
Encrypt data. | |
ECMAScript API | |
-------------- | |
klg.obfuscator.setKey(string key) | |
Set key to be used in decryption. | |
string klg.obfuscator.decode(string data) | |
Decrypt data. May throw errors if either key wasn't set | |
before or when decrypted data is not valid UTF-8. | |
string klg.obfuscator.href(HTMLAnchorElement a) | |
Convenience wrapper to decode address from xhref data attribute | |
(or title or name) and set it as actual href on given element. | |
Usage | |
----- | |
Page with data should first initialize Obfuscator with server secret | |
and simply use its `obfuscate' method whenever applicable. It should | |
also get the seed using `get_seed' method and put it in result along | |
the obfuscated data, for example as a query parameter in script tag | |
(but could be also stored somewhere else and converted to key with help | |
of XHR to make users' experience more pleasant). | |
Then ECMAScript code should take the seed and acquire associated key | |
from server and give it to `setKey' function after which `decode' function | |
(and `href') can be used directly. | |
See `demo.html' for simple self-contained example. | |
License | |
------- | |
This program is free software. It comes without any warranty, to | |
the extent permitted by applicable law. You can redistribute it | |
and/or modify it under the terms of the Do What The Fuck You Want | |
To Public License, Version 2, as published by Sam Hocevar. See | |
http://sam.zoy.org/wtfpl/COPYING for more details. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// Some random (but CONSTANT across requests!) data | |
$serverSecret = "The quick fire fox jumps over the lazy server."; | |
require_once 'Obfuscator.php'; | |
$o = new Obfuscator($serverSecret); | |
if ($_REQUEST['fallback']) { | |
// No javascript fallback page | |
if ($_POST['check'] == sha1($_REQUEST['fallback'] . $serverSecret) | |
&& preg_match('/^([0-9a-f]+)\.([0-9a-z_-]+)$/i',$_REQUEST['fallback'],$match)) { | |
// Reveal | |
$o->set_seed($match[1]); | |
$m = $o->deobfuscate($match[2]); | |
$m = preg_replace('/^mailto:(.*@.*)$/','<a href="\0">\1</a>',$m); | |
echo $m; | |
} | |
else { | |
// Ask | |
?> | |
<form action="" method="POST" enctype="multipart/form-data"> | |
<input type="hidden" id="fallback" name="fallback" value="<?php echo $_REQUEST['fallback']; ?>"> | |
<input type="hidden" id="check" name="check" value="<?php echo sha1($_REQUEST['fallback'] . $serverSecret); ?>"> | |
<input type="submit" value="Show email"> | |
</form> | |
<?php | |
} | |
?> | |
<?php | |
} | |
elseif ($_REQUEST['seed']) { | |
// Javascript with decryption key (second request) | |
$o->set_seed($_REQUEST['seed']); // no sanitization required | |
header("Content-type: application/ecmascript; charset=us-ascii"); | |
printf("klg.obfuscator.setKey(%s)", $o->get_key_json()); | |
} | |
else { | |
// The page with obfuscated data (first request) | |
?> | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Obfuscator demo</title> | |
<script type="application/ecmascript" src="obfuscator.min.js" defer="defer"></script> | |
<script type="application/ecmascript" src="?seed=<?php echo $o->get_seed(); ?>" defer="defer"></script> | |
</head> | |
<body> | |
<p> | |
Source should be self-explanatory. | |
Click <a href="javascript:alert('Error!')" onmouseover="klg.obfuscator.href(this)" | |
data-xhref="<?php echo $o->obfuscate("mailto:[email protected]"); ?>">here</a> | |
or <a onmouseover="klg.obfuscator.href(this)" | |
href="?fallback=<?php echo $o->get_seed() .'.'. | |
urlencode($o->obfuscate("mailto:[email protected]")) ?>">here</a> | |
to send me a mail. | |
</p> | |
<script type="application/ecmascript"> | |
window.onload = function() { | |
var a = document.createElement("p"); | |
a.innerHTML = klg.obfuscator.decode("<?php | |
echo $o->obfuscate("Everything works correctly if you see this line!"); | |
?>"); | |
document.body.appendChild(a); | |
} | |
</script> | |
</body> | |
</html> | |
<?php | |
} | |
?> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function(klg) { | |
"use strict"; | |
// Encryption key, must be set before using. | |
var key = undefined; | |
// Decrypt block of data with XXTEA algorithm. | |
var xxtea_dec = function(data, key) { | |
var z, y = data[0], e, DELTA = 0x9e3779b9; | |
var p, q = 0 | (6 + 52/data.length), sum = q*DELTA; | |
while (sum > 0) { | |
e = sum >>> 2; | |
for (p = data.length-1; p >= 0; p--) { | |
z = data[(data.length+p-1)%data.length]; | |
data[p] -= ((z>>>5)^(y<<2)) + ((y>>>3)^(z<<4)) ^ (sum^y) + (key[(p^e)&3]^z); | |
y = data[p] &= 0xffffffff; | |
} | |
sum -= DELTA; | |
} | |
return data; | |
}; | |
// Convert Base64url encoded data to array of words for decryption. | |
var unbase64url = (function(alpha) { | |
return function(str) { | |
var i, a32 = [], a8 = []; | |
for (i = 0; i < str.length; i += 4) { | |
a8.push((0|alpha[str.charCodeAt(i)]) << 2 | (0|alpha[str.charCodeAt(i+1)]) >>> 4); | |
a8.push(((0|alpha[str.charCodeAt(i+1)]) & 0xf) << 4 | (0|alpha[str.charCodeAt(i+2)]) >>> 2); | |
a8.push(((0|alpha[str.charCodeAt(i+2)]) & 0x3) << 6 | (0|alpha[str.charCodeAt(i+3)])); | |
} | |
for (i = 0; i+3 < a8.length; i += 4) | |
a32.push((a8[i]<<24)|(a8[i+1]<<16)|(a8[i+2]<<8)|a8[i+3]); | |
return a32; | |
}; | |
})((function() { | |
var a = [], s = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; | |
for (var i = 0; i < s.length; i++) | |
a[s.charCodeAt(i)] = i; | |
return a; | |
})()); | |
// Convert decrypted array of words to string. | |
// First word is ignored because it contains random IV. | |
var strunpack = function(a) { | |
var i, s = ""; | |
for (i = 1; i < a.length; i++) | |
s += String.fromCharCode( | |
(a[i]>>>24) & 0xff, (a[i]>>>16) & 0xff, | |
(a[i]>>> 8) & 0xff, (a[i]>>> 0) & 0xff); | |
i = s.length-1; | |
while (i >= 0 && !s.charCodeAt(i)) --i; | |
return s.substring(0, i+1); | |
}; | |
// Putting it all together. | |
var decode = function(xstr) { | |
var str = strunpack(xxtea_dec(unbase64url(xstr), key)); | |
return decodeURIComponent(escape(str)); // UTF-8 | |
}; | |
// Decrypt href attribute of an anchor. | |
var href = (function() { | |
var done = []; | |
return function(a) { | |
for (var i = 0; i < done.length; i++) | |
if (a == done[i]) | |
return a.href; | |
var e = a.dataset && a.dataset.xhref; | |
e = e || a.getAttribute("data-xhref"); | |
e = e || a.title || a.name; | |
e = e || decodeURIComponent((a.href.match("(?:^|[./;=?])([0-9A-Za-z!$%()*+,:^_`{|}~-]*)$")||[0,""])[1]); | |
if (e) { | |
done.push(a); | |
return a.href = decode(e); | |
} | |
}; | |
})(); | |
// Public interface | |
return klg["obfuscator"] = { | |
/** | |
* Set decryption key to use. | |
**/ | |
'setKey': function(k) { | |
key = k; | |
}, | |
/** | |
* De-obfuscate string. | |
* Throws URIError if decrypted string is not valid | |
* UTF-8 stream (likely because wrong decryption key). | |
**/ | |
'decode': decode, | |
/** | |
* Decrypt href attribute from xhref data attribute | |
* (or, if absent, from title or name for compatibility). | |
**/ | |
'href': href | |
}; | |
})("undefined" !== typeof module && module.exports || window.klg || (window.klg = {})); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function(j){"use strict";function h(c){for(var c=k(c),b=i,a,f=c[0],d,e,g=2654435769*(0|6+52/c.length);0<g;){d=g>>>2;for(e=c.length-1;0<=e;e--)a=c[(c.length+e-1)%c.length],c[e]-=(a>>>5^f<<2)+(f>>>3^a<<4)^(g^f)+(b[(e^d)&3]^a),f=c[e]&=4294967295;g-=2654435769}a="";for(b=1;b<c.length;b++)a+=String.fromCharCode(c[b]>>>24&255,c[b]>>>16&255,c[b]>>>8&255,c[b]>>>0&255);for(b=a.length-1;0<=b&&!a.charCodeAt(b);)--b;return decodeURIComponent(escape(a.substring(0,b+1)))}var i=void 0,k=function(c){return function(b){var a, | |
f=[],d=[];for(a=0;a<b.length;a+=4)d.push((0|c[b.charCodeAt(a)])<<2|(0|c[b.charCodeAt(a+1)])>>>4),d.push(((0|c[b.charCodeAt(a+1)])&15)<<4|(0|c[b.charCodeAt(a+2)])>>>2),d.push(((0|c[b.charCodeAt(a+2)])&3)<<6|0|c[b.charCodeAt(a+3)]);for(a=0;a+3<d.length;a+=4)f.push(d[a]<<24|d[a+1]<<16|d[a+2]<<8|d[a+3]);return f}}(function(){for(var c=[],b=0;64>b;b++)c["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".charCodeAt(b)]=b;return c}());return j.obfuscator={setKey:function(c){i=c},decode:h, | |
href:function(){var c=[];return function(b){for(var a=0;a<c.length;a++)if(b==c[a])return b.href;if(a=(a=(a=(a=b.dataset&&b.dataset.a)||b.getAttribute("data-xhref"))||b.title||b.name)||decodeURIComponent((b.href.match("(?:^|[./;=?])([0-9A-Za-z!$%()*+,:^_`{|}~-]*)$")||[0,""])[1]))return c.push(b),b.href=h(a)}}()}})("undefined"!==typeof module&&module.exports||window.klg||(window.klg={})); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Obfuscate UTF-8 strings in a way that allows easy decoding in Javascript | |
* while cryptographically forcing potentially misbehaving harvester to make | |
* additional request. | |
**/ | |
class Obfuscator { | |
const SEED_LENGTH = 16; | |
const KEY_ROUNDS = 20; | |
private $secret; | |
private $seed, $key; | |
public function __construct($secret, $seed = null) { | |
$this->secret = (string) $secret; | |
$this->seed = (string) $seed; | |
} | |
/** Get seed used to generate the key. */ | |
public function get_seed() { | |
if (!$this->seed) | |
$this->seed = self::make_seed(); | |
return $this->seed; | |
} | |
/** Set seed used to generate the key. */ | |
public function set_seed($seed) { | |
$this->key = null; | |
return $this->seed = (string) $seed; | |
} | |
/** Get encryption key as JSON string. */ | |
public function get_key_json() { | |
return '['.implode(',', $this->get_key()).']'; | |
} | |
/** Obfuscate a string by encrypting it. String should be UTF-8. */ | |
public function obfuscate($str) { | |
$k = $this->get_key(); | |
$s = self::strpack($str); | |
$x = self::xxtea_enc($s, $k); | |
return self::base64url($x); | |
} | |
/** Deobfuscate a string */ | |
public function deobfuscate($str) { | |
$k = $this->get_key(); | |
$x = self::unbase64url($str); | |
$s = self::xxtea_dec($x, $k); | |
return self::strunpack($s); | |
} | |
/** Get encryption key */ | |
private function get_key() { | |
if (!$this->key) | |
$this->key = self::make_key($this->get_seed()); | |
return $this->key; | |
} | |
/** Generate a seed. */ | |
private static function make_seed() { | |
$length = (int) ((self::SEED_LENGTH + 3) / 4); | |
$s = ""; | |
while ($length--) | |
$s .= sprintf("%08x", self::sha1rand()); | |
return $s; | |
} | |
/** Derive a key from the seed. */ | |
private function make_key($seed) { | |
$s = $this->secret . $seed . $this->secret; | |
$r = self::KEY_ROUNDS; | |
while ($r-- > 0) | |
$s = $this->secret . sha1($s, true) . $seed; | |
$s = sha1($s); | |
$a = array(); | |
$a[] = 0xffffffff & hexdec(substr($s, 0, 8)); | |
$a[] = 0xffffffff & hexdec(substr($s, 8, 8)); | |
$a[] = 0xffffffff & hexdec(substr($s,16, 8)); | |
$a[] = 0xffffffff & hexdec(substr($s,24, 8)); | |
return $a; | |
} | |
/** Secure-ish PRNG based on the SHA1 primitive. */ | |
private static function sha1rand($more_entropy = false) { | |
static $pad, $ctr = 0; | |
// seeding | |
if (!$ctr) { | |
$pad = @implode("\x1f", @array_values(@fstat(@fopen(__FILE__, 'r')))); | |
$pad.= "\x1e". @implode("\x1f", @array_values($_REQUEST)); | |
$pad.= "\x1e". @implode("\x1f", @array_values($_SERVER)); | |
$more_entropy = true; | |
} | |
if ($more_entropy) { | |
$pad .= "\x1e". microtime() . rand() . uniqid(mt_rand(), true); | |
if ($krng = @fopen('/dev/urandom', 'rb')) { | |
if (function_exists('stream_set_read_buffer')) | |
@stream_set_read_buffer($krng, 0); | |
$pad .= @fread($krng, 20); | |
@fclose($krng); | |
} | |
} | |
// actual PRNG | |
$pad = sha1($pad ."\x1f". ++$ctr, true); | |
return 0xffffffff & hexdec(substr(sha1($pad), 0, 8)); | |
} | |
/** Convert string to array of words for encryption. */ | |
private static function strpack($str) { | |
do $str .= "\0\0\0\0"; | |
while (mt_rand(0,1)); | |
$arr = array_values(unpack('N*', $str)); | |
array_unshift($arr, self::sha1rand()); // 32-bit IV | |
return array_pad($arr, 2, 0); | |
} | |
/** Convert array of words to string. */ | |
private static function strunpack($arr) { | |
array_shift($arr); | |
$str = ''; | |
foreach ($arr as $word) | |
$str .= pack('N', $word); | |
return rtrim($str, "\0"); | |
} | |
/** Encrypt block of data with XXTEA algorithm. */ | |
private static function xxtea_enc($data, $key) { | |
$n = count($data); | |
$z = $data[$n-1]; | |
$q = (int) (6 + 52 / count($data)); | |
$s = 0; | |
while ($q-- > 0) { | |
$s = 0xffffffff & ($s + 0x9e3779b9); | |
$e = $s >> 2; | |
for ($p = 0; $p < $n; $p++) { | |
$y = $data[($p+1)%$n]; | |
$a = ($z >> 5 & 0x07ffffff) ^ $y << 2; | |
$b = ($y >> 3 & 0x1fffffff) ^ $z << 4; | |
$a = 0xffffffff & ($a + $b); | |
$b = 0xffffffff & (($s ^ $y) + ($key[($p ^ $e) & 3] ^ $z)); | |
$z = 0xffffffff & ($data[$p] + ($a ^ $b)); | |
$data[$p] = $z; | |
} | |
} | |
return $data; | |
} | |
/** Decrypt block of data with XXTEA algorithm. */ | |
private static function xxtea_dec($data, $key) { | |
$n = count($data); | |
#$z = $data[$n-1]; | |
$y = $data[0]; | |
$q = (int) (6 + 52 / count($data)); | |
$s = 0xffffffff & ($q * 0x9e3779b9); | |
while ($q-- > 0) { | |
$e = $s >> 2; | |
for ($p = $n-1; $p >= 0; $p--) { | |
$z = $data[($n+$p-1)%$n]; | |
$a = ($z >> 5 & 0x07ffffff) ^ $y << 2; | |
$b = ($y >> 3 & 0x1fffffff) ^ $z << 4; | |
$a = 0xffffffff & ($a + $b); | |
$b = 0xffffffff & (($s ^ $y) + ($key[($p ^ $e) & 3] ^ $z)); | |
$y = 0xffffffff & ($data[$p] - ($a ^ $b)); | |
$data[$p] = $y; | |
} | |
$s = 0xffffffff & ($s - 0x9e3779b9); | |
} | |
return $data; | |
} | |
/** Encode 32b words as safe text. */ | |
private static function base64url($arr) { | |
$str = ''; | |
foreach ($arr as $word) | |
$str .= pack('N', $word); | |
$str = base64_encode($str); | |
return str_replace(array('+','/','='), array('-','_',''), $str); | |
} | |
/** Decode base64url to 32b words */ | |
static function unbase64url($str) { // FIXME: private | |
$str = str_replace(array('-','_'), array('+','/'), $str); | |
$str = base64_decode($str); | |
return array_values(unpack('N*', $str)); | |
} | |
} | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment