From f21fbe13c26217a611f88d9802db1f4692b40cd9 Mon Sep 17 00:00:00 2001 From: Thomas Lackner Date: Mon, 31 Dec 2012 05:49:27 -0600 Subject: [PATCH] first checkin of code --- README.md | 86 +++++++++++++ examples/fetch_all_new_photos.php | 39 ++++++ snaphax.php | 194 ++++++++++++++++++++++++++++++ 3 files changed, 319 insertions(+) create mode 100644 README.md create mode 100644 examples/fetch_all_new_photos.php create mode 100644 snaphax.php diff --git a/README.md b/README.md new file mode 100644 index 0000000..4202afc --- /dev/null +++ b/README.md @@ -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) + diff --git a/examples/fetch_all_new_photos.php b/examples/fetch_all_new_photos.php new file mode 100644 index 0000000..541552a --- /dev/null +++ b/examples/fetch_all_new_photos.php @@ -0,0 +1,39 @@ +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(); + diff --git a/snaphax.php b/snaphax.php new file mode 100644 index 0000000..e93f42d --- /dev/null +++ b/snaphax.php @@ -0,0 +1,194 @@ + + + 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×tamp=$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; + } + } +