first checkin of code

This commit is contained in:
Thomas Lackner 2012-12-31 05:49:27 -06:00
parent cbf6b1c463
commit f21fbe13c2
3 changed files with 319 additions and 0 deletions

86
README.md Normal file
View file

@ -0,0 +1,86 @@
Snaphax: a PHP library to use the Snapchat API
==============================================
This library allows you to communicate with Snapchat's servers using their
undocumented HTTP API. It was reverse engineered from the official Android
client (version 1.6)
Warning
-------
I made this by reverse engineering the app. It may be extremely buggy or piss
off the Snapchat people.
Limitations
-----------
Only login (with list of new media) and fetching of images is implemented.
This is obviously a huge failing which I am to correct when I have more time.
Motivation
----------
I'm a huge fan of Snapchat. I'm stunned and delighted by the fact that a simple
feature like auto-expiration of images can create such a compelling and
challenging service. And it's not just me: everyone I've told about Snapchat
who has used it has loved it.
But I hate closed APIs, so I set about figuring out how it worked. [Adam
Caudill](http://adamcaudill.com/2012/06/16/snapchat-api-and-security/) wrote an
excellent analysis of their HTTP-based API by using an HTTPS traffic sniffer.
Unfortunately this information now seems out of date.
I ended up having to fetch the official Android client's app binary (APK),
decompiling the whole thing with a mix of tools (all of them seemed to produce
subtly incorrect output), and then puzzling through the process of creating
their dreaded access tokens (called req\_token in the HTTP calls).
Their system is a bit unusual: it AES-256 hashes the two input values
separately, using a secret key contained in the binary, and then uses a fixed
pattern string to pull bytes from one or the other. The final composition of
the two is used in HTTP requests.
Other things about the API that I've discovered so far:
- Speaks JSON over HTTPS, using POST as the verb
- Not made for human consumption; difficult error messaging
- Doesn't seem to support JSONP (i.e., callback parameter in post data is
ignored)
How to use
----------
Pretty simple:
```
require_once('snaphax/snaphax.php');
$opts = array();
$opts['username'] = 'username';
$opts['password'] = 'password';
$opts['debug'] = 1; // CHANGE THIS; major spewage
$s = new Snaphax($opts);
$result = $s->login();
var_dump($result);
```
The apocalyptic future
----------------------
The TODO list is almost endless at this point:
- Syncing (to mark snaps as seen)
- Video fetching
- Image/video posting
- Friend list maintenance
- Port to Javascript (probably via Node + NPM since their API doesn't seem to
support JSONP)
- Add support for PHP composer
Author
------
Made by [@tlack](http://twitter.com/tlack) with a lot of help from
[@adamcaudill](http://twitter.com/adamcaudill)

View file

@ -0,0 +1,39 @@
<?
/*
Snaphax example: fetch all your new photo snaps
Run interactively like:
$ fetch_all_new_photos.php [username] [password]
*/
require_once('../snaphax.php');
function main() {
global $argv;
if (count($argv) != 3) {
die("$argv[0]: usage $argv[0] [user] [pass]\n");
}
$opts = array();
$opts['username'] = $argv[1];
$opts['password'] = $argv[2];
$opts['debug'] = 1; // CHANGE THIS; major spewage
$s = new Snaphax($opts);
$result = $s->login();
var_dump($result);
foreach ($result['snaps'] as $snap) {
var_dump($snap['st']);
if ($snap['st'] == SnapHax::STATUS_NEW) {
echo "fetching $snap[id]";
$blob_data = $s->fetch($snap['id']);
if ($blob_data)
file_put_contents($snap['id'].'.jpg', $blob_data);
}
}
}
main();

194
snaphax.php Normal file
View file

@ -0,0 +1,194 @@
<?
/*
SnapHax: a library for communicating with Snaphax
Implements a subset of the Snaphax API
(c) Copyright 2012 Thomas Lackner <lackner@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
$SNAPHAX_DEFAULT_OPTIONS = array(
'blob_enc_key' => 'M02cnQ51Ji97vwT4',
'debug' => false,
'pattern' => '0001110111101110001111010101111011010001001110011000110001000110',
'secret' => 'iEk21fuwZApXlz93750dmW22pw389dPwOk',
'static_token' => 'm198sOkJEn37DjqZ32lpRu76xmw288xSQ9',
'url' => 'https://feelinsonice.appspot.com'
);
if (!function_exists('curl_init')) {
throw new Exception('Snaphax needs the CURL PHP extension.');
}
if (!function_exists('json_decode')) {
throw new Exception('Snaphax needs the JSON PHP extension.');
}
class SnaphaxApi {
function SnaphaxApi($options) {
$this->options = $options;
}
private function debug($text) {
if ($this->options['debug'])
echo "SNAPHAX DEBUG: $text\n";
}
public function blob($snap_id, $username, $auth_token) {
$un = urlencode($username);
$ts = time();
// $token = $this->hash($ts, $auth_token);
// $url = "https://feelinsonice.appspot.com/ph/blob?id=$snap_id&username=$un&timestamp=$ts&req_token=$token";
$url = "/ph/blob";
$result = $this->postToEndpoint($url, array(
'id' => $snap_id,
'timestamp' => $ts,
'username' => $username,
), $auth_token, $ts, 0);
$this->debug('blob result: ' . $result);
$result_decoded = mcrypt_decrypt('rijndael-128', $this->options['blob_enc_key'], $result, 'ecb');
$this->debug('decoded: ' . $result_decoded);
if ($result_decoded[0] == chr(0xFF) &&
$result_decoded[1] == chr(0xD8)) {
return $result_decoded;
} else
return false;
}
public function httpGet($url) {
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL, $this->options['url'].$url);
curl_setopt($ch,CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch,CURLOPT_USERAGENT,'Snaphax 4.0.1 (iPad; iPhone OS 6.0; en_US)');
$this->debug($url);
// execute post
$result = curl_exec($ch);
$this->debug($result);
// close connection
curl_close($ch);
return json_decode($result, true);
}
public function postToEndpoint($endpoint, $post_data, $param1, $param2, $json=1) {
$ch = curl_init();
// set the url, number of POST vars, POST data
curl_setopt($ch,CURLOPT_URL, $this->options['url'].$endpoint);
curl_setopt($ch,CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch,CURLOPT_USERAGENT,'Snaphax 4.0.1 (iPad; iPhone OS 6.0; en_US)');
$post_data['req_token'] = $this->hash($param1, $param2);
curl_setopt($ch,CURLOPT_POST, count($post_data));
curl_setopt($ch,CURLOPT_POSTFIELDS, http_build_query($post_data));
$this->debug(json_encode($post_data));
// execute post
$result = curl_exec($ch);
$this->debug($result);
// close connection
curl_close($ch);
if ($json)
return json_decode($result, true);
else
return $result;
}
function hash($param1, $param2) {
$this->debug("p1: $param1");
$this->debug("p2: $param2");
$s1 = $this->options['secret'] . $param1;
$this->debug("s1: $s1");
$s2 = $param2 . $this->options['secret'];
$this->debug("s2: $s2");
$hash = hash_init('sha256');
hash_update($hash, $s1);
$s3 = hash_final($hash, false);
$this->debug("s3: $s3");
$hash = hash_init('sha256');
hash_update($hash, $s2);
$s4 = hash_final($hash, false);
$this->debug("s4: $s4");
$out = '';
for ($i = 0; $i < strlen($this->options['pattern']); $i++) {
$c = $this->options['pattern'][$i];
if ($c == '0')
$out .= $s3[$i];
else
$out .= $s4[$i];
}
$this->debug("out: $out");
return $out;
}
}
class Snaphax {
const STATUS_NEW = 1;
function Snaphax($options) {
global $SNAPHAX_DEFAULT_OPTIONS;
$this->options = array_merge($SNAPHAX_DEFAULT_OPTIONS, $options);
$this->api = new SnaphaxApi($this->options);
$this->auth_token = false;
}
private function error($text) {
error_report($text);
return false;
}
function login() {
$ts = time();
$out = $this->api->postToEndpoint(
'/ph/login',
array(
'username' => $this->options['username'],
'password' => $this->options['password'],
'timestamp' => $ts
),
$this->options['static_token'],
$ts
);
if (is_array($out) &&
!empty($out['auth_token'])) {
$this->auth_token = $out['auth_token'];
}
return $out;
}
function fetch($id) {
if (!$this->auth_token) {
return $this->error('no auth token');
}
$blob = $this->api->blob($id,
$this->options['username'],
$this->auth_token);
return $blob;
}
}