diff --git a/README.md b/README.md index 8135340..2425440 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,22 @@ composer require mgp25/snapapi ``` +## Getting a Casper API Key + +This is required for the API to work. + +Go to https://clients.casper.io/login.php and create an account. + +Once you have created an account, go to "Projects" and create a new project. + +![projects](http://s2.postimg.org/r7olutpah/projects.png) + +Now you will have your project with your API Key and API Secret. + +![api](http://s2.postimg.org/vi39qeudl/api.png) + +You will need to set this data in the constructor, as shown in the [examples] (/examples). + ### Special thanks - [teknogeek](https://github.com/teknogeek) diff --git a/examples/exampleBots/friendBot.php b/examples/exampleBots/friendBot.php index 3f97e56..660c953 100644 --- a/examples/exampleBots/friendBot.php +++ b/examples/exampleBots/friendBot.php @@ -3,15 +3,17 @@ include_once("../../src/snapchat.php"); /////////// DATA ///////////// -$username = ''; -$password = ''; -$gEmail = ''; -$gPasswd = ''; -$debug = false; +$username = ''; +$password = ''; +$gEmail = ''; +$gPasswd = ''; +$casperKey = ''; +$casperSecret = ''; +$debug = false; ////////////////////////////// // Login -$snapchat = new Snapchat($username, $gEmail, $gPasswd, $debug); +$snapchat = new Snapchat($username, $gEmail, $gPasswd, $casperKey, $casperSecret, $debug); $snapchat->login($password); // Get unconfirmed friends diff --git a/examples/exampleFunctional.php b/examples/exampleFunctional.php index 3f282ed..daecf0e 100644 --- a/examples/exampleFunctional.php +++ b/examples/exampleFunctional.php @@ -3,10 +3,12 @@ require_once("../src/snapchat.php"); //////////// CONFIG //////////// -$username = ""; // Your snapchat username -$password = ""; // Your snapchat password -$gEmail = ""; // Gmail account -$gPasswd = ""; // Gmail account password +$username = ""; // Your snapchat username +$password = ""; // Your snapchat password +$gEmail = ""; // Gmail account +$gPasswd = ""; // Gmail account password +$casperKey = ""; // Casper API Key +$casperSecret = ""; // Casper API Secret $debug = false; // Set this to true if you want to see all outgoing requests and responses from server //////////////////////////////// @@ -14,7 +16,7 @@ $imagePath = ""; // URL or local path to a media file (image or video) $sendTo = array(); -$snapchat = new Snapchat($username, $gEmail, $gPasswd, $debug); +$snapchat = new Snapchat($username, $gEmail, $gPasswd, $casperKey, $casperSecret, $debug); //Login to Snapchat with your username and password $snapchat->login($password); diff --git a/examples/registerTool.php b/examples/registerTool.php index 9002315..eca307e 100644 --- a/examples/registerTool.php +++ b/examples/registerTool.php @@ -21,7 +21,13 @@ echo "\nGmail password: "; $gPasswd = trim(fgets(STDIN)); -$snapchat = new Snapchat($username, $gMail, $gPasswd, true); +echo "\nCasper key: "; +$casperKey = trim(fgets(STDIN)); + +echo "\nCasper secret: "; +$casperSecret = trim(fgets(STDIN)); + +$snapchat = new Snapchat($username, $gMail, $gPasswd, $casperKey, $casperSecret, true); $id = $snapchat->register($username, $password, $email, $birthday); @@ -35,15 +41,16 @@ $result = trim(fgets(STDIN)); $result = $snapchat->sendCaptcha($result, $id); -unlink(__DIR__."{$id}"); -if ($result == null) +unlink(__DIR__.DIRECTORY_SEPARATOR."..".DIRECTORY_SEPARATOR."src".DIRECTORY_SEPARATOR.$id); +if(property_exists($result, "error") && $result->error === 0 && property_exists($result->data, "find_friends_enabled")) { echo "Account successfully created\n"; echo "\nUsername: $username\n"; echo "Password: $password\n"; echo "Email: $email\n"; } -else { +else +{ echo "There was an error registering your account\n"; echo "Error code: " . $result['code']; -} +} \ No newline at end of file diff --git a/examples/verifyPhone.php b/examples/verifyPhone.php index 3f7a458..728e81c 100644 --- a/examples/verifyPhone.php +++ b/examples/verifyPhone.php @@ -15,16 +15,22 @@ echo "\nGmail password: "; $gPasswd = trim(fgets(STDIN)); -echo "\Phone number: "; +echo "\nCasper key: "; +$casperKey = trim(fgets(STDIN)); + +echo "\nCasper secret: "; +$casperSecret = trim(fgets(STDIN)); + +echo "\nPhone number: "; $phone = trim(fgets(STDIN)); -$snapchat = new Snapchat($username, $gEmail, $gPasswd, true); +$snapchat = new Snapchat($username, $gEmail, $gPasswd, $casperKey, $casperSecret, true); $snapchat->login($password); $snapchat->sendPhoneVerification($phone); -echo "\Code: "; +echo "\nCode: "; $code = trim(fgets(STDIN)); $snapchat->verifyPhoneNumber($code); diff --git a/src/Casper-API-PHP/CasperAPI.php b/src/Casper-API-PHP/CasperAPI.php new file mode 100644 index 0000000..93f3ff9 --- /dev/null +++ b/src/Casper-API-PHP/CasperAPI.php @@ -0,0 +1,143 @@ + $username, + "password" => $password, + "timestamp" => $timestamp, + "snapchat_version" => self::SNAPCHAT_VERSION + )); + + if(!isset($response->signature)){ + throw new CasperException("Signature not found in Response"); + } + + return $response->signature; + + } + + /** + * Fetches an Attestation by making multiple API calls to the Google and Casper APIs. + * + * @param string $nonce + * Base64 encoded value of the nonce + * sha256(username|password|timestamp|/loq/login) + * + * @return string + * The Client Auth Token + * + * @throws CasperException + * An exception is thrown if an error occurs. + */ + public function getSnapchatAttestation($nonce){ + + $response = parent::get("/snapchat/attestation/create"); + + if(!isset($response->binary)){ + throw new CasperException("Binary not found in Response"); + } + + $binary = base64_decode($response->binary); + + $response = parent::externalRequest("https://www.googleapis.com/androidantiabuse/v1/x/create?alt=PROTO&key=AIzaSyBofcZsgLSS7BOnBjZPEkk4rYwzOIz-lTI", array( + "Content-Type: application/x-protobuf", + "User-Agent: SafetyNet/7899000 (klte KOT49H); gzip" + ), $binary, true); + + $protobuf = base64_encode($response); + + $response = parent::post("/snapchat/attestation/attest", null, array( + "protobuf" => $protobuf, + "nonce" => $nonce, + "snapchat_version" => self::SNAPCHAT_VERSION + )); + + if(!isset($response->binary)){ + throw new CasperException("Binary not found in Response"); + } + + $binary = base64_decode($response->binary); + + $response = parent::externalRequest("https://www.googleapis.com/androidcheck/v1/attestations/attest?alt=JSON&key=AIzaSyDqVnJBjE5ymo--oBJt3On7HQx9xNm1RHA", array( + "Content-Type: application/x-protobuf", + "User-Agent: SafetyNet/7899000 (klte KOT49H); gzip" + ), $binary, true); + + $json = json_decode($response); + if($json == null){ + throw new CasperException("Failed to decode response!"); + } + + if(!isset($json->signedAttestation)){ + throw new CasperException("Attestation not found in Response"); + } + + return $json->signedAttestation; + + } + + /** + * Generates an Nonce for Attestation requests. + * + * @param string $username + * Snapchat Username + * + * @param string $password + * Snapchat Password + * + * @param string $timestamp + * Snapchat Login Timestamp + * + * @param string $endpoint + * Snapchat Login Endpoint, always /loq/login at this stage. + * + * @return string + * The Base64 Encoded Nonce + * + * @throws CasperException + * An exception is thrown if an error occurs. + */ + public function generateSnapchatNonce($username, $password, $timestamp, $endpoint = "/loq/login"){ + return base64_encode(hash("sha256", "{$username}|{$password}|{$timestamp}|{$endpoint}", true)); + } + +} \ No newline at end of file diff --git a/src/Casper-API-PHP/CasperAgent.php b/src/Casper-API-PHP/CasperAgent.php new file mode 100644 index 0000000..3ac60d5 --- /dev/null +++ b/src/Casper-API-PHP/CasperAgent.php @@ -0,0 +1,286 @@ + 10, + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_USERAGENT => self::USER_AGENT, + CURLOPT_HEADER => false, + CURLINFO_HEADER_OUT => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "gzip" + ); + + public function setAPIKey($api_key = null){ + $this->API_KEY = $api_key; + } + + public function setAPISecret($api_secret = null){ + $this->API_SECRET = $api_secret; + } + + /** + * Performs a GET request. + * + * @param string $endpoint + * Endpoint to make GET request to. + * + * @param array $headers + * An array of parameters to send in the request. + * + * @return data + * The response data. + * + * @throws CasperException + * An exception is thrown if an error occurs. + */ + public function get($endpoint, $headers = array()){ + return $this->request($endpoint, $headers); + } + + /** + * Performs a POST request. + * + * @param string $endpoint + * Endpoint to make POST request to. + * + * @param array $headers + * An array of parameters to send in the request. + * + * @param array $params + * An array of parameters to send in the request. + * + * @return data + * The response data. + * + * @throws CasperException + * An exception is thrown if an error occurs. + */ + public function post($endpoint, $headers = array(), $params = array()){ + return $this->request($endpoint, $headers, $params, true); + } + + /** + * Performs a POST request. + * + * @param string $endpoint + * Endpoint to make request to. + * + * @param array $headers + * An array of parameters to send in the request. + * + * @param array $params + * An array of parameters to send in the request. + * + * @param boolean $post + * true to make a POST request, else a GET request will be made. + * + * @return stdClass + * The JSON data returned from the API. + * + * @throws CasperException + * An exception is thrown if an error occurs. + */ + public function request($endpoint, $headers = array(), $params = array(), $post = false){ + + $ch = curl_init(); + + if($headers == null){ + $headers = array(); + } + + if($params == null){ + $params = array(); + } + + $headers = array_merge(self::$CURL_HEADERS, $headers); + + $headers[] = "X-Casper-API-Key: " . $this->API_KEY; + $headers[] = "X-Casper-Signature: " . $this->generateRequestSignature($params, $this->API_SECRET); + + curl_setopt_array($ch, self::$CURL_OPTIONS); + curl_setopt($ch, CURLOPT_URL, self::URL . $endpoint); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + //curl_setopt($ch, CURLOPT_PROXY, "http://localhost:8888"); + + if($post){ + if(is_array($params)){ + $params = http_build_query($params); + } + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $params); + } + + $result = curl_exec($ch); + + //If cURL doesn't have a bundle of root certificates handy, + //we provide ours (see http://curl.haxx.se/docs/sslcerts.html). + if(curl_errno($ch) == 60){ + curl_setopt($ch, CURLOPT_CAINFO, "../ca_bundle.crt"); + $result = curl_exec($ch); + } + + //If the cURL request fails, return FALSE. + if($result === FALSE){ + $error = curl_error($ch); + curl_close($ch); + throw new CasperException($error); + } + + $json = json_decode($result); + if($json == null){ + curl_close($ch); + throw new CasperException("Failed to decode response!"); + } + + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if($code != 200){ + + curl_close($ch); + + if(isset($json->code) && isset($json->message)){ + throw new CasperException("API Response: [{$json->code}] {$json->message}"); + } else { + throw new CasperException("API Response: [{$code}] Unknown Error Message"); + } + + } + + curl_close($ch); + + return $json; + + } + + /** + * Performs a POST request. + * + * @param string $url + * Url to make request to. + * + * @param array $headers + * An array of parameters to send in the request. + * + * @param array $params + * An array of parameters to send in the request. + * + * @param boolean $post + * true to make a POST request, else a GET request will be made. + * + * @return stdClass + * The JSON data returned from the API. + * + * @throws CasperException + * An exception is thrown if an error occurs. + */ + public function externalRequest($url, $headers = array(), $params = array(), $post = false){ + + $ch = curl_init(); + + if($headers == null){ + $headers = array(); + } + + if($params == null){ + $params = array(); + } + + curl_setopt_array($ch, self::$CURL_OPTIONS); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + if($post){ + if(is_array($params)){ + $params = http_build_query($params); + } + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $params); + } + + $result = curl_exec($ch); + + //If cURL doesn't have a bundle of root certificates handy, + //we provide ours (see http://curl.haxx.se/docs/sslcerts.html). + if(curl_errno($ch) == 60){ + curl_setopt($ch, CURLOPT_CAINFO, "../ca_bundle.crt"); + $result = curl_exec($ch); + } + + //If the cURL request fails, return FALSE. + if($result === FALSE){ + curl_close($ch); + throw new CasperException("cURL request failed!"); + } + + if(curl_getinfo($ch, CURLINFO_HTTP_CODE) != 200){ + + curl_close($ch); + throw new CasperException("External Request Failed!"); + + } + + curl_close($ch); + + return $result; + + } + + + /** + * + * Generates an HMAC Signature of the Request Parameters + * Parameter Keys need to be in alphabetical order. + * Values are then appended to the end. + * + * @param array $parameters Parameters sent in your Request + * @param string $secret Your Casper API Secret + * @return string + */ + public function generateRequestSignature($parameters = array(), $secret){ + + ksort($parameters); + + $string = ""; + foreach($parameters as $key => $value){ + $string .= $key.$value; + } + return "v1:".hash_hmac("sha256", $string, $secret); + } + +} \ No newline at end of file diff --git a/src/Casper-API-PHP/CasperException.php b/src/Casper-API-PHP/CasperException.php new file mode 100644 index 0000000..c000dd2 --- /dev/null +++ b/src/Casper-API-PHP/CasperException.php @@ -0,0 +1,9 @@ +username = $username; - $this->debug = $debug; - $this->gEmail = $gEmail; - $this->gPasswd = $gPasswd; + $this->username = $username; + $this->debug = $debug; + $this->gEmail = $gEmail; + $this->gPasswd = $gPasswd; + $this->casper = new CasperAPI($casperKey, $casperSecret); - if(file_exists(__DIR__ . DIRECTORY_SEPARATOR . self::DATA_FOLDER . DIRECTORY_SEPARATOR . "auth-$this->username.dat")){ + if(file_exists(__DIR__ . DIRECTORY_SEPARATOR . self::DATA_FOLDER . DIRECTORY_SEPARATOR . "auth-$this->username.dat")) + { $this->totArray = unserialize(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . self::DATA_FOLDER . DIRECTORY_SEPARATOR . "auth-$this->username.dat")); } - if(array_key_exists($this->username, $this->totArray[0])){ + + if(array_key_exists($this->username, $this->totArray[0])) + { $this->auth_token = $this->totArray[0][$this->username]; } - if(array_key_exists($this->username, $this->totArray[1])){ + + if(array_key_exists($this->username, $this->totArray[1])) + { if($this->totArray[1][$this->username][1] > time()) parent::setGAuth($this->totArray[1][$this->username][0]); } } @@ -137,98 +145,11 @@ public function device() return $result; } - - - public function getAttestation($password, $timestamp) + public function getAttestation($username, $password, $timestamp) { - $binary = file_get_contents("https://api.casper.io/droidguard/create/binary"); - $binaryJSON = json_decode($binary); - - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, "https://www.googleapis.com/androidantiabuse/v1/x/create?alt=PROTO&key=AIzaSyBofcZsgLSS7BOnBjZPEkk4rYwzOIz-lTI"); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); - curl_setopt($ch, CURLOPT_ENCODING, "gzip"); - curl_setopt($ch, CURLOPT_USERAGENT, "DroidGuard/7329000 (A116 _Quad KOT49H); gzip"); - curl_setopt($ch, CURLOPT_POST, TRUE); - curl_setopt($ch, CURLOPT_POSTFIELDS, base64_decode($binaryJSON->binary)); - curl_setopt($ch, CURLOPT_HTTPHEADER, array("Accept:", "Expect:", "content-type: application/x-protobuf")); - - $return = curl_exec($ch); - - if(curl_getinfo($ch, CURLINFO_HTTP_CODE) != 200) - { - throw new Exception("attestationCreate Exception: HTTP Status Code != 200"); - } - - curl_close($ch); - - $ch = curl_init(); - - curl_setopt($ch, CURLOPT_URL, "https://api.casper.io/droidguard/attest/binary"); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); - curl_setopt($ch, CURLOPT_POST, TRUE); - curl_setopt($ch, CURLOPT_POSTFIELDS, array( - "bytecode_proto" => base64_encode($return), - "nonce" => base64_encode(hash("sha256", $this->username."|{$password}|{$timestamp}|/loq/login", true)), - "apk_digest" => "5O40Rllov9V8PpwD5zPmmp+GQi7UMIWz2A0LWZA7UX0=" - )); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); - - $return = curl_exec($ch); - - if(curl_getinfo($ch, CURLINFO_HTTP_CODE) != 200) - { - throw new Exception("getAttestation Exception: HTTP Status Code != 200"); - } - - curl_close($ch); - - $return = json_decode($return); - - if(!$return || !isset($return->binary)) - { - throw new Exception("getAttestation Exception: Invalid JSON / No signedAttestation returned"); - } - - $postData = base64_decode($return->binary); - - $ch = curl_init(); - - curl_setopt($ch, CURLOPT_URL, "https://www.googleapis.com/androidcheck/v1/attestations/attest?alt=JSON&key=AIzaSyDqVnJBjE5ymo--oBJt3On7HQx9xNm1RHA"); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); - curl_setopt($ch, CURLOPT_HEADER, FALSE); - curl_setopt($ch, CURLOPT_POST, TRUE); - curl_setopt($ch, CURLOPT_POSTFIELDS, $postData); - curl_setopt($ch, CURLOPT_HTTPHEADER, array( - 'Accept:', - 'Expect:', - 'User-Agent: SafetyNet/7899000 (WIKO JZO54K); gzip', - 'Content-Type: application/x-protobuf', - 'Content-Length: ' . strlen($postData), - 'Connection: Keep-Alive' - )); - curl_setopt($ch, CURLOPT_ENCODING, "gzip"); - - $return = curl_exec($ch); - - if(curl_getinfo($ch, CURLINFO_HTTP_CODE) != 200) - { - throw new Exception("getAttestation Exception: HTTP Status Code != 200"); - } - - curl_close($ch); - - $return = json_decode($return); - - if(!$return || !isset($return->signedAttestation)) - { - throw new Exception("getAttestation Exception: Invalid JSON / No signedAttestation returned"); - } - - return $return->signedAttestation; + $nonce = $this->casper->generateSnapchatNonce($username, $password, $timestamp); + $attestation = $this->casper->getSnapchatAttestation($nonce); + return $attestation; } public function encryptPassword($email, $password) @@ -343,39 +264,8 @@ public function getAuthToken() public function getClientAuthToken($username, $password, $timestamp) { - $data = array( - "username" => $username, - "password" => $password, - "timestamp" => $timestamp - ); - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, "https://api.casper.io/security/login/signrequest/"); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); - curl_setopt($ch, CURLINFO_HEADER_OUT, TRUE); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); - curl_setopt($ch, CURLOPT_HEADER, FALSE); - curl_setopt($ch, CURLOPT_ENCODING, "gzip"); - curl_setopt($ch, CURLOPT_POST, TRUE); - curl_setopt($ch, CURLOPT_POSTFIELDS, $data); - $return = curl_exec($ch); - - if(curl_getinfo($ch, CURLINFO_HTTP_CODE) != 200) - { - $return["error"] = 1; - $return["data"] = "HTTP Status Code != 200"; - - return $return; - } - curl_close($ch); - $return = json_decode($return, true); - if(!$return || $return["code"] != 200 || !isset($return["signature"])) - { - $return["error"] = 1; - $return["data"] = "Invalid JSON / Incorrect status / No signature returned."; - } - - return $return; + $clientAuthToken = $this->casper->getSnapchatClientAuth($username, $password, $timestamp); + return $clientAuthToken; } private function getGCMToken() @@ -469,7 +359,7 @@ public function login($password, $force = FALSE) return $auth; } parent::setGAuth($auth); - $attestation = $this->getAttestation($password, $timestamp); + $attestation = $this->getAttestation($this->username, $password, $timestamp); $clientAuthToken = $this->getClientAuthToken($this->username, $password, $timestamp); $result = parent::post( @@ -494,7 +384,7 @@ public function login($password, $force = FALSE) parent::STATIC_TOKEN, $timestamp, $auth['auth'], - $clientAuthToken["signature"] + $clientAuthToken ), $multipart = false, $debug = $this->debug @@ -625,7 +515,7 @@ public function register($username, $password, $email, $birthday, $phone_verific return $return; } - $attestation = $this->getAttestation($password, $timestamp); + $attestation = $this->getAttestation($this->username, $password, $timestamp); $birthDate = explode("-", $birthday); $age = (date("md", date("U", mktime(0, 0, 0, $birthDate[0], $birthDate[1], $birthDate[2]))) > date("md") ? ((date("Y") - $birthDate[2]) - 1) : (date("Y") - $birthDate[2])); @@ -656,7 +546,7 @@ public function register($username, $password, $email, $birthday, $phone_verific $this->auth_token = $result['data']->auth_token; $auth = $this->getAuthToken(); - $this->totArray[1][$this->username] = array($auth, time()+(55*60)); + $this->totArray[1][$this->username] = array($auth, time() + (55 * 60)); file_put_contents(__DIR__ . DIRECTORY_SEPARATOR . self::DATA_FOLDER . DIRECTORY_SEPARATOR . "auth-$this->username.dat", serialize($this->totArray)); if($auth['error'] == 1) { @@ -1067,7 +957,7 @@ public function getUpdates($force = TRUE) if(strlen(parent::getGAuth()) <= 0){ $a = $this->getAuthToken(); parent::setGAuth($a); - $this->totArray[1][$this->username] = array($a, time()+(55*60)); + $this->totArray[1][$this->username] = array($a, time() + (55 * 60)); file_put_contents(__DIR__ . DIRECTORY_SEPARATOR . self::DATA_FOLDER . DIRECTORY_SEPARATOR . "auth-$this->username.dat",serialize($this->totArray)); } $timestamp = parent::timestamp(); diff --git a/src/snapchat_agent.php b/src/snapchat_agent.php index d238596..ff9867f 100644 --- a/src/snapchat_agent.php +++ b/src/snapchat_agent.php @@ -11,7 +11,7 @@ abstract class SnapchatAgent { * Before updating this value, confirm * that the library requests everything in the same way as the app. */ - const USER_AGENT = 'Snapchat/9.14.2.0 (HTC One; Android 4.4.2#302626.7#19; gzip)'; + const USER_AGENT = 'Snapchat/9.16.2.0 (HTC One; Android 5.0.2#482424.2#21; gzip)'; /* * The API URL. We're using the /bq endpoint, the one that the iPhone