├── .gitignore ├── LICENSE ├── README.md ├── examples ├── fetch_all_new_photos.php ├── req_token.php └── upload.php └── snaphax.php /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *swo 3 | *swp 4 | examples/*jpg 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Thomas Lackner 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the “Software”), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Snaphax: a PHP library to use the Snapchat API 2 | ============================================== 3 | 4 | This library allows you to communicate with Snapchat's servers using their 5 | undocumented HTTP API. It was reverse engineered from the official Android 6 | client (version 1.6) 7 | 8 | Warning 9 | ------- 10 | 11 | I made Snaphax by reverse engineering the app. It may be extremely buggy or 12 | piss off the Snapchat people. Use at your own risk. 13 | 14 | How to use 15 | ---------- 16 | 17 | Pretty simple: 18 | 19 | ``` 20 | require_once('snaphax/snaphax.php'); 21 | 22 | $opts = array(); 23 | $opts['username'] = 'username'; 24 | $opts['password'] = 'password'; 25 | $opts['debug'] = 1; 26 | 27 | $s = new Snaphax($opts); 28 | $result = $s->login(); 29 | var_dump($result); 30 | ``` 31 | 32 | Limitations 33 | ----------- 34 | 35 | Only login (with list of new media) and fetching of image/video snaps is 36 | implemented. This is obviously a huge failing which I am to correct when I 37 | have more time. 38 | 39 | Motivation and development process 40 | ---------------------------------- 41 | 42 | I'm a huge fan of Snapchat, a photo/video sharing app that allows you to set 43 | expiration times on the media you send to your friends. They can't open it 44 | after they've seen it for up to 10 seconds, and if they take a screenshot, the 45 | other party is notified. 46 | 47 | I'm stunned and delighted by the fact that a simple 48 | feature like auto-expiration of images can create such a compelling and 49 | challenging service. And it's not just me: everyone I've told about Snapchat 50 | who has used it has loved it, and as of last November more than one billion 51 | snaps had been exchanged using the service. 52 | 53 | But I hate closed products, so I set about figuring out how it worked. [Adam 54 | Caudill](http://adamcaudill.com/2012/06/16/snapchat-api-and-security/) wrote an 55 | excellent analysis of their HTTP-based API by using an HTTPS traffic sniffer. 56 | Unfortunately this information now seems out of date. 57 | 58 | I ended up having to fetch the official Android client's app binary (APK), 59 | decompiling the whole thing with a mix of tools (all of them seemed to produce 60 | subtly incorrect output), tracing the control flow a bit, and then puzzling 61 | through the process of creating their dreaded access tokens (called req\_token 62 | in the HTTP calls). 63 | 64 | This involved me paging through Fiddler, trying to generate SHA-256 hashes 65 | seemingly at random, tearing my heart out, and weeping openly. 66 | 67 | Their system is a bit unusual: it AES-256 hashes two input values separately, 68 | using a secret key contained in the binary, and then uses a fixed pattern 69 | string to pull bytes from one or the other. The final composition of the two is 70 | used in HTTP requests. Why not just append the values pre-hash? The security 71 | profile would be similar. 72 | 73 | Other things about the API that I've discovered so far: 74 | 75 | - Speaks JSON over HTTPS, using POST as the verb 76 | - Not made for human consumption; difficult error messaging 77 | - Doesn't seem to support JSONP (i.e., callback parameter in post data is 78 | ignored) 79 | - Blob (image/video) downloads are encrypted using AES. This code successfully 80 | decodes them before they are returned by the library. 81 | 82 | The apocalyptic future 83 | ---------------------- 84 | 85 | The TODO list is almost endless at this point: 86 | 87 | - API likely to change 88 | - DOCS!!! 89 | - Figure out the /device call - what's this do? also device_id in /login resp 90 | - Syncing (to mark snaps as seen) 91 | - Image/video uploading 92 | - Friend list maintenance 93 | - Port to Javascript (probably via Node + NPM since their API doesn't seem to 94 | support JSONP) 95 | - Add support for PHP composer 96 | - Test framework 97 | 98 | License 99 | ------- 100 | 101 | MIT 102 | 103 | Credits 104 | ------- 105 | 106 | Made by Thomas Lackner <[@tlack](http://twitter.com/tlack)> with a lot of help 107 | from [@adamcaudill](http://twitter.com/adamcaudill). And of course none of 108 | this would be possible without the inventiveness of the 109 | [Snapchat](http://snapchat.com) team 110 | 111 | -------------------------------------------------------------------------------- /examples/fetch_all_new_photos.php: -------------------------------------------------------------------------------- 1 | login(); 26 | var_dump($result); 27 | if (empty($result) || empty($result['snaps'])) { 28 | echo "no snaps"; 29 | exit; 30 | } 31 | 32 | foreach ($result['snaps'] as $snap) { 33 | if ($snap['st'] == SnapHax::STATUS_NEW) { 34 | echo "fetching $snap[id]\n"; 35 | $blob_data = $s->fetch($snap['id']); 36 | if ($blob_data) { 37 | if ($snap['m'] == SnapHax::MEDIA_IMAGE) 38 | $ext = '.jpg'; 39 | else 40 | $ext = '.mp4'; 41 | file_put_contents($snap['sn'].$snap['id'].$ext, $blob_data); 42 | } 43 | } 44 | } 45 | } 46 | 47 | main(); 48 | 49 | -------------------------------------------------------------------------------- /examples/req_token.php: -------------------------------------------------------------------------------- 1 | reqToken($argv[3], $argv[4]); 20 | echo "req_token for $argv[3] $argv[4]:\n"; 21 | var_dump($req_token); 22 | $req_token = $s->reqToken($argv[4], $argv[3]); 23 | echo "req_token for $argv[4] $argv[3]:\n"; 24 | var_dump($req_token); 25 | } 26 | 27 | main(); 28 | -------------------------------------------------------------------------------- /examples/upload.php: -------------------------------------------------------------------------------- 1 | login(); 23 | var_dump($result); 24 | $result = $s->upload($fdata, SnapHax::MEDIA_IMAGE, array($argv[4])); 25 | var_dump($result); 26 | } 27 | 28 | main(); 29 | -------------------------------------------------------------------------------- /snaphax.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | */ 26 | 27 | $SNAPHAX_DEFAULT_OPTIONS = array( 28 | 'blob_enc_key' => 'M02cnQ51Ji97vwT4', 29 | 'debug' => false, 30 | 'pattern' => '0001110111101110001111010101111011010001001110011000110001000110', 31 | 'secret' => 'iEk21fuwZApXlz93750dmW22pw389dPwOk', 32 | 'static_token' => 'm198sOkJEn37DjqZ32lpRu76xmw288xSQ9', 33 | 'url' => 'https://feelinsonice-hrd.appspot.com', 34 | 'user_agent' => 'Snaphax 4.0.1 (iPad; iPhone OS 6.0; en_US)' 35 | ); 36 | 37 | if (!function_exists('curl_init')) { 38 | throw new Exception('Snaphax needs the CURL PHP extension.'); 39 | } 40 | if (!function_exists('json_decode')) { 41 | throw new Exception('Snaphax needs the JSON PHP extension.'); 42 | } 43 | if (!function_exists('mcrypt_decrypt')) { 44 | throw new Exception('Snaphax needs the mcrypt PHP extension.'); 45 | } 46 | 47 | class Snaphax { 48 | // High level class to perform actions on Snapchat 49 | 50 | const STATUS_NEW = 1; 51 | const MEDIA_IMAGE = 0; 52 | const MEDIA_VIDEO = 1; 53 | 54 | function Snaphax($options) { 55 | global $SNAPHAX_DEFAULT_OPTIONS; 56 | 57 | $this->options = array_merge($SNAPHAX_DEFAULT_OPTIONS, $options); 58 | $this->api = new SnaphaxApi($this->options); 59 | $this->auth_token = false; 60 | } 61 | function login() { 62 | $ts = $this->api->time(); 63 | $out = $this->api->postCall( 64 | '/bq/login', 65 | array( 66 | 'username' => $this->options['username'], 67 | 'password' => $this->options['password'], 68 | 'timestamp' => $ts 69 | ), 70 | $this->options['static_token'], 71 | $ts 72 | ); 73 | if (is_array($out) && 74 | !empty($out['auth_token'])) { 75 | $this->auth_token = $out['auth_token']; 76 | } 77 | return $out; 78 | } 79 | function fetch($id) { 80 | if (!$this->auth_token) { 81 | throw new Exception('no auth token'); 82 | } 83 | $ts = $this->api->time(); 84 | $url = "/bq/blob"; 85 | $result = $this->api->postCall($url, array( 86 | 'id' => $id, 87 | 'timestamp' => $ts, 88 | 'username' => urlencode($this->options['username']) 89 | ), $this->auth_token, $ts, 0); 90 | $this->api->debug('blob result', $result); 91 | 92 | // some blobs are not encrypted 93 | if ($this->api->isValidBlobHeader(substr($result, 0, 256))) { 94 | $this->api->debug('blob not encrypted'); 95 | return $result; 96 | } 97 | 98 | $result_decoded = $this->api->decrypt($result); 99 | $this->api->debug('decoded snap', $result_decoded); 100 | if ($this->api->isValidBlobHeader(substr($result_decoded, 0, 256))) { 101 | return $result_decoded; 102 | } else { 103 | $this->api->debug('invalid image/video data header'); 104 | return false; 105 | } 106 | } 107 | function reqToken($param1, $param2) { 108 | return $this->api->hash($param1, $param2); 109 | } 110 | function upload($file_data, $type, $recipients, $time=8) { 111 | if ($type != self::MEDIA_IMAGE && $type != self::MEDIA_VIDEO) 112 | throw new Exception('Snaphax: upload type must be MEDIA_IMAGE or MEDIA_VIDEO'); 113 | if (!$this->auth_token) { 114 | throw new Exception('no auth token'); 115 | } 116 | if (!is_array($recipients)) 117 | $recipients = array($recipients); 118 | $ts = $this->api->time(); 119 | $media_id = strtoupper($this->options['username']).time(); 120 | $this->api->debug('upload snap data', $file_data); 121 | $file_data_encrypted = $this->api->encrypt($file_data); 122 | $this->api->debug('upload snap data encrypted', $file_data_encrypted); 123 | file_put_contents('/tmp/blah.jpg', $file_data_encrypted); 124 | $result = $this->api->postCall( 125 | '/bq/upload', 126 | array( 127 | 'username' => $this->options['username'], 128 | 'timestamp' => $ts, 129 | 'type' => $type, 130 | // 'data' => urlencode($file_data_encrypted).'; filename="file"', 131 | 'data' => '@/tmp/blah.jpg;filename=file', 132 | 'media_id' => $media_id 133 | ), 134 | $this->auth_token, 135 | $ts, 136 | 0, 137 | array("Content-type: multipart/form-data") 138 | ); 139 | $this->api->debug('upload result', $result); 140 | 141 | foreach ($recipients as $recipient) { 142 | $ts = $this->api->time(); 143 | $result = $this->api->postCall( 144 | '/bq/send', 145 | array( 146 | 'username' => $this->options['username'], 147 | 'timestamp' => $ts, 148 | 'recipient' => $recipient, 149 | 'media_id' => $media_id, 150 | 'time' => $time 151 | ), 152 | $this->auth_token, 153 | $ts, 154 | 0 155 | ); 156 | $this->api->debug("send to $recipient: " . $result); 157 | } 158 | 159 | return $media_id; 160 | } 161 | } 162 | 163 | class SnaphaxApi { 164 | // Low level code to communicate with Snapchat via HTTP 165 | 166 | function SnaphaxApi($options) { 167 | $this->options = $options; 168 | } 169 | 170 | function debug($text, $binary = false) { 171 | if ($this->options['debug']) { 172 | echo "SNAPHAX DEBUG: $text"; 173 | if ($binary !== false) { 174 | // shortened hex repr of binary 175 | $len = strlen($binary); 176 | $tmp = " hex ($len bytes): "; 177 | $tmp.= join(' ', array_map('dechex', array_map('ord', str_split(substr($binary, 0, 16))))); 178 | $tmp.= ' ... '; 179 | $tmp.= join(' ', array_map('dechex', array_map('ord', str_split(substr($binary, -16))))); 180 | echo $tmp; 181 | } 182 | echo "\n"; 183 | } 184 | } 185 | 186 | function isValidBlobHeader($header) { 187 | if (($header[0] == chr(00) && // mp4 188 | $header[0] == chr(00)) || 189 | ($header[0] == chr(0xFF) && // jpg 190 | $header[1] == chr(0xD8))) 191 | return true; 192 | else 193 | return false; 194 | } 195 | 196 | function decrypt($data) { 197 | return mcrypt_decrypt('rijndael-128', $this->options['blob_enc_key'], $data, 'ecb'); 198 | } 199 | 200 | function encrypt($data) { 201 | return mcrypt_encrypt('rijndael-128', $this->options['blob_enc_key'], $data, 'ecb'); 202 | } 203 | 204 | public function postCall($endpoint, $post_data, $param1, $param2, $json=1, $headers=false) { 205 | $ch = curl_init(); 206 | 207 | // set the url, number of POST vars, POST data 208 | curl_setopt($ch,CURLOPT_URL, $this->options['url'].$endpoint); 209 | curl_setopt($ch,CURLOPT_RETURNTRANSFER, 1); 210 | curl_setopt($ch,CURLOPT_USERAGENT, $this->options['user_agent']); 211 | 212 | if ($headers && is_array($headers)) { 213 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 214 | } 215 | 216 | $post_data['req_token'] = $this->hash($param1, $param2); 217 | curl_setopt($ch, CURLOPT_POST, count($post_data)); 218 | if (!$headers) 219 | curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data)); 220 | else 221 | curl_setopt($ch,CURLOPT_POSTFIELDS, $post_data); 222 | $this->debug('POST params: ' . json_encode($post_data)); 223 | $result = curl_exec($ch); 224 | if ($result === false) { 225 | $this->debug('CURL error: '.curl_error($ch)); 226 | return false; 227 | } 228 | $this->debug('HTTP response code' . curl_getinfo($ch, CURLINFO_HTTP_CODE)); 229 | $this->debug('POST return ' . $result); 230 | 231 | // close connection 232 | curl_close($ch); 233 | 234 | if ($json) 235 | return json_decode(utf8_encode($result), true); 236 | else 237 | return $result; 238 | } 239 | 240 | function hash($param1, $param2) { 241 | $this->debug("p1: $param1"); 242 | $this->debug("p2: $param2"); 243 | 244 | $s1 = $this->options['secret'] . $param1; 245 | $this->debug("s1: $s1"); 246 | $s2 = $param2 . $this->options['secret']; 247 | $this->debug("s2: $s2"); 248 | 249 | $hash = hash_init('sha256'); 250 | hash_update($hash, $s1); 251 | $s3 = hash_final($hash, false); 252 | $this->debug("s3: $s3"); 253 | 254 | $hash = hash_init('sha256'); 255 | hash_update($hash, $s2); 256 | $s4 = hash_final($hash, false); 257 | $this->debug("s4: $s4"); 258 | 259 | $out = ''; 260 | for ($i = 0; $i < strlen($this->options['pattern']); $i++) { 261 | $c = $this->options['pattern'][$i]; 262 | if ($c == '0') 263 | $out .= $s3[$i]; 264 | else 265 | $out .= $s4[$i]; 266 | } 267 | $this->debug("out: $out"); 268 | return $out; 269 | } 270 | 271 | function time() { 272 | return round(microtime(true) * 1000); 273 | } 274 | 275 | } 276 | 277 | 278 | ?> --------------------------------------------------------------------------------