Skip to content

Instantly share code, notes, and snippets.

@mwgamera
Created September 12, 2012 02:19
Show Gist options
  • Save mwgamera/3703797 to your computer and use it in GitHub Desktop.
Save mwgamera/3703797 to your computer and use it in GitHub Desktop.
Ultimate obfuscator for e-mail addresses
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.
<?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:foo@example.com"); ?>">here</a>
or <a onmouseover="klg.obfuscator.href(this)"
href="?fallback=<?php echo $o->get_seed() .'.'.
urlencode($o->obfuscate("mailto:bar@example.net")) ?>">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
}
?>
(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 = {}));
(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={}));
<?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