I'm using a simple PHP script to verify Android order to parse download for the customer.
$receipt = $_GET['purchaseData'];
$billInfo = json_decode($receipt,true);
$signature = $_GET['dataSignature'];
$public_key_base64 = "xxxxxxxxxxxxxxxx";
$key = "-----BEGIN PUBLIC KEY-----\n".
chunk_split($public_key_base64, 64,"\n").
'-----END PUBLIC KEY-----';
$key = openssl_get_publickey($key);
$signature = base64_decode($signature);
//$result = openssl_verify($billInfo, $signature, $key);
$result = openssl_verify($receipt, $signature, $key);
if (0 === $result) {
echo "0";
} else if (1 !== $result) {
echo "1";
} else {
echo "Hello World!";
}
//added the var_dump($result); as asked by A-2-A
var_dump($result);
result is 0int(0)
I made a real order through the App after I published it and when trying to validate the order I get "0" as result.
I tried direct HTTP access
https://domain.com/thankyou.php?purchaseData={"packageName":"com.example.app","orderId":"GPA.1234-5678-1234-98608","productId":"product","developerPayload":"mypurchasetoken","purchaseTime":1455346586453,"purchaseState":0,"developerPayload":"mypurchasetoken","purchaseToken":"ggedobflmccnemedgplmodhp...."}&dataSignature=gwmBf...
I'm keeping the first of the question because my result is still a guess. After further investigation I think it's the signature not being read in a nice clean way as sent by google.
The signature=gwmBfgGudpG5iPp3L0OnepNlx while the browser is reading it as ƒ ~®v‘¹ˆúw
How is it possible to let it be read in the right way?
To verify the signature you want to make sure of the following:
INAPP_PURCHASE_DATA is not mutated in any way. Any encoding or escaping changes will result in a invalid verification. The best way to ensure it gets to your server intact is to base64 encoded it.
INAPP_DATA_SIGNATURE also must remain intact, it should already base64 encoded so sending that to your server should not be a problem.
openssl_verify expects both data and signature arguments to be in their raw state, so base64 decode before verifying.
It also takes signature_alg as the last argument, in this case sha1WithRSAEncryption should work as should the default, but if in doubt try a few other sha1 algorithms to see which ones work.
My best guess why it's not working for you right now is that you're not receiving the INAPP_PURCHASE_DATA on your server in the same condition that it was received on the app. This Stackoverflow question had the same problem.
Related
I have been working on a way to implement HMAC verification in python with flask for the selly.gg merchant website.
So selly's dev documentation give these following examples to verify HMAC signatures (in PHP and ruby): https://developer.selly.gg/?php#signing-validating
(code below:)
PHP:
<?php
$signature = hash_hmac('sha512', json_encode($_POST), $secret);
if hash_equals($signature, $signatureFromHeader) {
// Webhook is valid
}
?>
RUBY:
signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha512'), secret, payload.to_json)
is_valid_signature = ActiveSupport::SecurityUtils.secure_compare(request.headers['X-Selly-Signature'], signature)
So, so far what I could figure out: They don't encode with base64 (like shopify and others do), it uses SHA-512, it encodes the secret code alongside json response data and finally the request header is 'X-Selly-Signature'
I've made the following code so far (based on shopify's code for HMAC signing https://help.shopify.com/en/api/getting-started/webhooks):
SECRET = "secretkeyhere"
def verify_webhook(data, hmac_header):
digest = hmac.new(bytes(SECRET, 'ascii'), bytes(json.dumps(data), 'utf8'), hashlib.sha512).hexdigest()
return hmac.compare_digest(digest, hmac_header)
try:
responsebody = request.json #line:22
status = responsebody['status']#line:25
except Exception as e:
print(e)
return not_found()
print("X Selly sign: " + request.headers.get('X-Selly-Signature'))
verified = verify_webhook(responsebody, request.headers.get('X-Selly-Signature'))
print(verified)
However selly has a webhook simulator, and even with the proper secret key and valid requests, the verify_webhook will always return False. I tried contacting Selly support, but they couldn't help me more than that
You can test the webhook simulator at the following address:
https://selly.io/dashboard/{your account}/developer/webhook/simulate
You're nearly right except that you don't need to json.dumps the request data. This will likely introduce changes into output, such as changes to formatting, that won't match the original data meaning the HMAC will fail.
E.g.
{"id":"fd87d909-fbfc-466c-964a-5478d5bc066a"}
is different to:
{
"id":"fd87d909-fbfc-466c-964a-5478d5bc066a"
}
which is actually:
{x0ax20x20"id":"fd87d909-fbfc-466c-964a-5478d5bc066a"x0a}
A hash will be completely different for the two inputs.
See how json.loads and json.dumps will modify the formatting and therefore the hash:
http_data = b'''{
"id":"fd87d909-fbfc-466c-964a-5478d5bc066a"
}
'''
print(http_data)
h = hashlib.sha512(http_data).hexdigest()
print(h)
py_dict = json.loads(http_data) # deserialise to Python dict
py_str = json.dumps(py_dict) # serialise to a Python str
py_bytes = json.dumps(py_dict).encode('utf-8') # encode to UTF-8 bytes
print(py_str)
h2 = hashlib.sha512(py_bytes).hexdigest()
print(h2)
Output:
b'{\n "id":"fd87d909-fbfc-466c-964a-5478d5bc066a"\n}\n'
364325098....
{"id": "fd87d909-fbfc-466c-964a-5478d5bc066a"}
9664f687a....
It doesn't help that Selly's PHP example shows something similar. In fact, the Selly PHP example is useless as the data won't be form encoded anyway, so the data won't be in $_POST!
Here's my little Flask example:
import hmac
import hashlib
from flask import Flask, request, Response
app = Flask(__name__)
php_hash = "01e5335ed340ef3f211903f6c8b0e4ae34c585664da51066137a2a8aa02c2b90ca13da28622aa3948b9734eff65b13a099dd69f49203bc2d7ae60ebee9f5d858"
secret = "1234ABC".encode("ascii") # returns a byte object
#app.route("/", methods=['POST', 'GET'])
def selly():
request_data = request.data # returns a byte object
hm = hmac.new(secret, request_data, hashlib.sha512)
sig = hm.hexdigest()
resp = f"""req: {request_data}
sig: {sig}
match: {sig==php_hash}"""
return Response(resp, mimetype='text/plain')
app.run(debug=True)
Note the use of request.data to get the raw byte input and the simple use of encode on the secret str to get the encoded bytes (instead of using the verbose bytes() instantiation).
This can be tested with:
curl -X "POST" "http://localhost:5000/" \
-H 'Content-Type: text/plain; charset=utf-8' \
-d "{\"id\":\"fd87d909-fbfc-466c-964a-5478d5bc066a\"}"
I also created a bit of PHP to validate both languages create the same result:
<?php
header('Content-Type: text/plain');
$post = file_get_contents('php://input');
print $post;
$signature = hash_hmac('sha512', $post, "1234ABC");
print $signature;
?>
How to receive raw JSON response from HTTP POST webhook?
I am working with an API and to verify that the POST to the webhook is indeed from the appropriate company API, they suggest this method:
To allow a client to verify a webhook message has in fact come from SIGNIFYD, an X-SIGNIFYD-SEC-HMAC-SHA256 header is included in each webhook POST message. The contents of this header is the Base64 encoded output of the HMAC SHA256 encoding of the JSON body of the message, using the team's API key as the encryption key. To verify the authenticity of the webhook message, you should calculate this value yourself and verify it equals the value contained in the header.
For the test environment, the "secret" key is ABCDE instead of the "Team API key."
I am receiving it in PHP like so:
<?php
// Get relevant Signifyd custom headers to be used for verification
$header_sig_topic = $_SERVER['HTTP_X_SIGNIFYD_TOPIC'];
$header_sig_sec_hmac = $_SERVER['HTTP_X_SIGNIFYD_SEC_HMAC_SHA256'];
// Get POST body
$webhookContent = "";
$webhook = fopen('php://input' , 'r');
while (!feof($webhook)) {
$webhookContent .= fread($webhook, 4096);
}
fclose($webhook);
?>
then I am processing it into the hash like so:
<?php
$sig_ver_sha = hash_hmac('sha256', $webhookContent, $secret);
$sig_ver_hash = base64_encode( $sig_ver_sha );
?>
However, I am going wrong somewhere, because the hash I calculate is
OTc1YzExZDY2ZTE1MTVmYmJmNWNhNDRhNWMxZGIzZDk0NmM3OGE4NDU2N2JkYTJmZDJlYWI0ODRhNjlhNTdiYg==
while the header for an identical sample response header always comes with
W+D70ded8u5DG7P4BcG0u2etvAqQZvxz70Q4OXh0vlY=
I thought I was getting the JSOn body wrong somehow so I've tried every combination of json_encode and json_decode but nothing helps, my hash never matches.
I've also tried using $webhookContent = json_decode(file_get_contents('php://input'), true); to store the POST body but that just comes up empty ($_POST doesn't work either).
Am I doing something else wrong other than receiving the JSON?
The JSON that comes as the body of the test response which always comes with W+D70ded8u5DG7P4BcG0u2etvAqQZvxz70Q4OXh0vlY= as the hash key to be used for verification:
{ "analysisUrl": "https://signifyd.com/v2/cases/1/analysis",
"entriesUrl": "https://signifyd.com/v2/cases/1/entries", "notesUrl":
"https://signifyd.com/v2/cases/1/notes", "orderUrl":
"https://signifyd.com/v2/cases/1/order", "guaranteeEligible":false,
"status":"DISMISSED", "uuid":"709b9107-eda0-4cdd-bdac-a82f51a8a3f3",
"headline":"John Smith", "reviewDisposition":null, "associatedTeam":{
"teamName":"anyTeam", "teamId":26, "getAutoDismiss":true,
"getTeamDismissalDays":2 }, "orderId":"19418",
"orderDate":"2013-06-17T06:20:47-0700", "orderAmount":365.99,
"createdAt":"2013-11-05T14:23:26-0800",
"updatedAt":"2013-11-05T14:23:26-0800",
"adjustedScore":262.6666666666667, "investigationId":1,
"score":262.6666666666667, "caseId":1,
"guaranteeDisposition":"APPROVED"}
If it helps to see where I'm going wrong, an example is provided but it's in Python:
Mac sha256HMAC = javax.crypto.Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(teamAPIKEY.getBytes(), "HmacSHA256");
sha256HMAC.init(secretKey);
String encodedHMAC256 = Base64.encodeBase64String(sha256HMAC.doFinal(jsonBody.getBytes("UTF-8")));
My error was in simply not specifying the $raw_output parameter of the hash_hmac() function as true.
raw_output
When set to TRUE, outputs raw binary data. FALSE outputs lowercase hexits.
So, since I wasn't specifying $raw_output as true, I was getting hexits instead of raw binary output, which looked like this: 975c11d66e1515fbbf5ca44a5c1db3d946c78a84567bda2fd2eab484a69a57bb.
EDIT: The answer here is
<?php
$sig_ver_sha = hash_hmac('sha256', $webhookContent, $secret, true);
$sig_ver_hash = base64_encode( $sig_ver_sha );
?>
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 2 days ago.
Improve this question
edited code
foreach($parsed_xml->OperationRequest->Errors->Error as $error){
echo "Error code: " . $error->Code . "\r\n";
echo $error->Message . "\r\n"; echo "\r\n";
}
}
function printSearchResults($parsed_xml, $SearchIndex){
print("<table>");
if($numOfItems>0){
foreach($parsed_xml->Items->Item as $current){
print("<td><font size='-1'><b>".$current->ItemAttributes->Title."</b>");
if (isset($current->ItemAttributes->Title)) {
print("<br>Title: ".$current->ItemAttributes->Title);
} elseif(isset($current->ItemAttributes->Author)) {
print("<br>Author: ".$current->ItemAttributes->Author);
} elseif(isset($current->Offers->Offer->Price->FormattedPrice)){
print("<br>Price:".$current->Offers->Offer->Price->FormattedPrice);
}else{
print("<center>No matches found.</center>");
}
}
}
}
if (!isset($params["Timestamp"])) {
$params["Timestamp"] = gmdate('Y-m-d\TH:i:s\Z');
}
ksort($params);
$pairs = array();
foreach ($params as $key => $value) {
array_push($pairs, rawurlencode($key)."=".rawurlencode($value));
}
$canonical_query_string = join("&", $pairs);
$string_to_sign = "GET\n".$endpoint."\n".$uri."\n".$canonical_query_string;
$signature = base64_encode(hash_hmac("sha256", $string_to_sign, $aws_secret_key, true));
$request_url = 'http://'.$endpoint.$uri.'?'.$canonical_query_string.'&Signature='.rawurlencode( $signature);
echo "Signed URL: \"".$request_url."\"";
}
function fetchDataUsingAPI($UniqueBatchId) {
echo $this->firstpageurl;
if( isset($_POST['datafetchresume'])){
$lastbatchdetails = GetTaskLastBatchDetails($_GET['taskid']);
$UniqueBatchId = $lastbatchdetails[0];
$this->firstpageurl = $lastbatchdetails[1];
}else {
$this->firstpageurl = $this->ItemSearch($categorySplit[1],$UniqueBatchId);
}
SaveTaskLastBatchDetails($_GET['taskid'], $UniqueBatchId, $this- >firstpageurl);
return $UniqueBatchId;
Every request needs a signature to be calculated and added to the URL. Check out my PHP function for adding a signature.
Also, use CURL rather than file_get_contents. Even though the API returned a 400 error, it also returned XML with it. That XML probably contains helpful error message. If you use CURL, you can detect the 400 error and still read the contents of the page that was returned. With file_get_contents, a 400 error just returns an error. (My recollection of that may be off, but you'll like CURL better.)
Check here for more info on Amazon API error code
In the PHP code, there are three variables left undefined by me. MY_ASSOCIATE_ID, MY_PUBLIC_KEY, MY_PRIVATE_KEY must be defined by you with appropriate values. The two keys are given to you by amazon. You can, for a while, get them here:
https://portal.aws.amazon.com/gp/aws/securityCredentials
Or check the "My Account" menu at the top of this page and go hunting for your security credentials. The "new and improved" system has been reported to not work with this API, so I would get your two keys from the link above while you still can. Store them in a safe place.
The associate ID can be anything--the API doesn't check it. The API uses uses your URL parameters to generate a signature with your public key, which is included in the URL, and the secret key they have on file associated with the public key you provided. The signature you provide in the URL must match theirs exactly, so your keys and your sig function must exactly match theirs.
I'm trying to work with the examples on the Twitter dev site but can't seem to get to the same signature as they have.
I am trying to complete step 3 on https://dev.twitter.com/docs/auth/implementing-sign-twitter because I am getting an error "Invalid or expired token" but I know it isn't because I've only just been given it, so it must be something wrong with my data packet.
The code I am using to try and generate this is:
// testing bit
$oauth = array(
'oauth_consumer_key'=>'cChZNFj6T5R0TigYB9yd1w',
'oauth_nonce'=>'a9900fe68e2573b27a37f10fbad6a755',
'oauth_signature_method'=>'HMAC-SHA1',
'oauth_timestamp'=>'1318467427',
'oauth_token'=>'NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0',
'oauth_version'=>'1.0'
);
$this->o_secret = 'LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE';
$this->c_secret = 'kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw';
ksort($oauth);
$string = rawurlencode(http_build_query($oauth));
$new_string = strtoupper($http_method).'&'.rawurlencode($main_url[0]).'&'.$string;
// The request_token request doesn't need a o_secret because it doesn't have one!
$sign_key = strstr($fullurl,'request_token') ? $this->c_secret.'&' : $this->c_secret.'&'.$this->o_secret;
echo urlencode(base64_encode(hash_hmac('sha1',$new_string,$sign_key,true)));exit;
And I'm assuming that the keys listed on this page are in fact correct: https://dev.twitter.com/docs/auth/creating-signature. So in that case the signature should be 39cipBtIOHEEnybAR4sATQTpl2I%3D.
If you can spot what I'm missing that would be great.
Your consumer secret and token secret are incorrect for the page you reference. If you look further up the page you can see that they should be:
Consumer secret: L8qq9PZyRg6ieKGEKhZolGC0vJWLw8iEJ88DRdyOg
Token secret: veNRnAWe6inFuo8o2u8SLLZLjolYDmDP7SzL0YfYI
Also in Step 3 you need to include the oauth_verifier in the list of parameters when calculating your signature base string.
I'm not familiar with PHP so I haven't checked your code to calculate the signature.
This code has now worked - I will tidy it up from there :)
// This function is to help work out step 3 in the process and why it is failing
public function testSignature(){
// testing bit
$oauth = array(
'oauth_consumer_key'=>'cChZNFj6T5R0TigYB9yd1w',
'oauth_nonce'=>'a9900fe68e2573b27a37f10fbad6a755',
'oauth_signature_method'=>'HMAC-SHA1',
'oauth_timestamp'=>'1318467427',
'oauth_token'=>'NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0',
'oauth_version'=>'1.0'
);
$this->o_secret = 'LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE';
$this->c_secret = 'kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw';
ksort($oauth);
$string = http_build_query($oauth);
$new_string = strtoupper($http_method).'&'.$main_url[0].'&'.$string;
$new_string = 'POST&https%3A%2F%2Fapi.twitter.com%2F1%2Fstatuses%2Fupdate.json&include_entities%3Dtrue%26oauth_consumer_key%3Dxvz1evFS4wEEPTGEFPHBog%26oauth_nonce%3DkYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1318622958%26oauth_token%3D370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb%26oauth_version%3D1.0%26status%3DHello%2520Ladies%2520%252B%2520Gentlemen%252C%2520a%2520signed%2520OAuth%2520request%2521';
// The request_token request doesn't need a o_secret because it doesn't have one!
$sign_key = $this->c_secret.'&'.$this->o_secret;
echo 'Should be: tnnArxj06cWHq44gCs1OSKk/jLY=<br>';
echo 'We get: '.base64_encode(hash_hmac('sha1',$new_string,$sign_key,true));
exit;
}
you want to access token from twitter and sign in implementation you can see in this example.
1) http://www.codexworld.com/login-with-twitter-using-php/
and this one for timeline tweets
2) http://www.codexworld.com/create-custom-twitter-widget-using-php/
may be this help you .
Using the blowfish cbc mode, I want to create an encrypted token. When I create the token and immediately decrypt it, it works correctly. However, if I place the encrypted token in SESSION and try to decrypt it during a subsequent request, the decryption produces garbage.
This is my code. I am using SESSION for testing; eventually, I'll be storing these strings in a database instead.
session_start();
define("key","v8nga4r76qlipm111jnioool");
define("iv",substr(md5(uniqid(rand(),1)),0,8));
require_once("Crypt/Blowfish.php");
$str = "Blowfish_test";
// encode start!!
$blowfish = Crypt_Blowfish::factory("cbc", key, iv);
$encrypt = $blowfish->encrypt($str);
$encrypt64 = base64_encode($encrypt);
$_SESSION["test"] = $encrypt64;
So far, everything is correct. If, on the same request, I immediately decode it (from SESSION), the results are as expected:
// decode start!!
$blowfish = Crypt_Blowfish::factory("cbc", key, iv);
$decrypt64 = base64_decode($_SESSION["test"]);
$decrypt = $blowfish->decrypt($decrypt64);
$trim_decrypt = rtrim($decrypt, "\0");
echo "Before encryption : " . $str ."<br>";
echo "Encrypted string   : " .$encrypt64. "<br>";
echo "decrypted string   : " .$trim_decrypt. "<br>";
If I place only the second block of code on a different page (so a previously generated string is being decrypted), the decrypted string is garbage.
Encryption -> session["test"] -> Decryption --- no problem
session["test"] -> Decryption --- problem
What's happening here?
I'm sorry for this super delay.
I solved this problem by the comment at that time, so I close this question using community wiki.
--- add ---
problem is solved
// encode
// At the same time save the initialization vector
$_SESSION["test"] = $encrypt64;
$_SESSION["iv"] = iv;
--- other file ---
// decode
$blowfish = Crypt_Blowfish::factory("cbc", key, $_SESSION["iv"]);