How to sign a payload with a private key in PHP? - php

I am trying to utilize the GoodRx API using PHP.
Here is my code:
$hash = hash_hmac('sha256', $query_string, MY_SECRET_KEY);
$encoded = base64_encode($hash);
$private_key = str_replace('+', '_', $encoded);
$private_key = str_replace('/', '_', $encoded);
//$private_key = urlencode($private_key);
$query_string .= '&sig=' . $private_key;
echo $query_string;
// https://api.goodrx.com/low-price?name=Lipitor&api_key=MY_API_KEY&sig=MY_SECRET_KEY
It is returning an error saying that my sig is not right.
Could you help me please.
Thank you.
Thomas.

Posting this in case anyone needs a complete example of how to make a basic call to the GoodRx API:
In Python:
import requests
import hashlib
import hmac
import base64
# python --version returns:
# Python 3.5.1 :: Anaconda 2.4.1 (32-bit)
# my_api_key is the API key for your account, provided by GoodRx
my_api_key = "YOUR_API_KEY";
# s_skey is the secret key for your account, provided by GoodRx
my_secret_key=str.encode("YOUR_SECRET_KEY", 'utf-8')
# Create the base URL and the URL parameters
# The url_params start as text and then have to be encoded into bytes
url_base = "https://api.goodrx.com/fair-price?"
url_params = str.encode("name=lipitor&api_key=" + my_api_key, 'utf-8')
# This does the hash of url_params with my_secret_key
tmpsig = hmac.new(my_secret_key, msg=url_params, digestmod=hashlib.sha256).digest()
# Base64 encoding gets rid of characters that can't go in to URLs.
# GoodRx specifically asks for these to be replaced with "_"
sig = base64.b64encode(tmpsig, str.encode("__", 'utf-8') )
# Convert the sig back to ascii
z = sig.decode('ascii')
# add the sig to the URL base
url_base += url_params.decode('ascii') + "&sig=" + z
# request the URL base
r = requests.get(url_base)
# print the response
print(r.content)
You should get something like this for the output:
{"errors": [], "data": {"mobile_url":
"http://m.goodrx.com/?grx_ref=api#/drug/atorvastatin/tablet", "form":
"tablet", "url": "http://www.goodrx.com/atorvastatin?grx_ref=api",
"brand": ["lipitor"], "dosage": "20mg", "price": 12.0, "generic":
["atorvastatin"], "quantity": 30, "display": "Lipitor (atorvastatin)",
"manufacturer": "generic"}, "success": true}
Here's similar code in PHP, looking up the drug Apidra Solostar, which has NDC 00088250205:
<?php
function base64url_encode($data) {
return strtr(base64_encode($data), '+/', '__');
}
$my_api_key = "YOUR_API_KEY";
$s_key="YOUR_SECRET_KEY";
$ndc = "00088250205";
// Initialize the CURL package. This is the thing that sends HTTP requests
$ch = curl_init();
// Create the URL and the hash
$url = "https://api.goodrx.com/fair-price?";
$query_string="ndc=" . $ndc . "&api_key=" . $my_api_key;
$tmp_sig = hash_hmac('sha256', $query_string, $s_key, true);
$sig = base64url_encode( $tmp_sig );
$url = $url . $query_string . "&sig=" . $sig;
// set some curl options
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_HEADER, FALSE);
curl_setopt($ch, CURLOPT_VERBOSE, true);
// run the query
$response = curl_exec($ch);
var_dump($response);
?>
Thanks to Thomas for talking with me on this.

You're not doing your string replacements correctly:
$private_key = str_replace('+', '_', $encoded);
^^---new string ^---original string
$private_key = str_replace('/', '_', $encoded);
^--overwrite previous replacement ^---with original string again
If you want to chain replacements, you have to do something more like:
$orig = 'foo';
$temp = str_replace(..., $orig);
$temp = str_replace(..., $temp);
...
$final = str_replace(..., $temp);
Note how you pass in the result of the PREVIOUS replacement into the next call, which is what you're not doing. You just keep taking the originals tring, replace one thing, then trash that replacement on the next call. So effectively you're ONLY doing a /->_ replacement, and sending the + as-is.

Related

PHP doing a SHA256 hash signature on a string

I am trying to sign a string with the secret key I have, but it keeps failing. I am new to this and are just starting to understand it a little bit (encryption and base64). But I have been stuck at this point for three days now and have pulled out all of my hairs. I just dont get it.
The bit from the api is:
MD5 Digest
MD5 algorithm is used to perform a signature operation on the final string to be signed, thereby obtaining a signature hex result string (this string is assigned to the parameter sign). In the MD5 calculation result, letters are all capitalized.
RSA Signature
Users could use their private key to perform a signature operation (Base64 coded) to a MD5 digest by implementing SHA256 algorithm through RSA, after users getting the signature result string which is signed by MD5 algorithm.
All I have tried:
$echostr="SS...AD";
$api_key="17...e0";
$secret="EB....D";
$parameters="api_key=".$api_key."&echostr=".$echostr."&signature_method=HmacSHA256&timestamp=".$timeStamp;
$preparedStr=base64_encode(strtoupper(md5($parameters)));
$sign=rsa_hash_sign($preparedStr, $secret);
echo "1 ".$sign."<\br>";
$sign=HmacSHA256_Sign($preparedStr, $secret);
echo "2 ".$sign."<\br>";
$sign=GenerateDigest($secret);
echo "3 ".$sign."<\br>";
$binaryKey = decode($secret);
$sign=encode(hash_hmac("sha256", $preparedStr, $binaryKey, true));
echo "4 ".$sign."<\br>";
$sign=hash_hmac("sha256", $preparedStr, $binaryKey, true);
echo "5 ".$sign."<\br>";
$sign=base64_encode(hash_hmac('sha256', base64_encode($preparedStr), $secret,true));
echo "6 ".$sign."<br>";
$cURLConnection = curl_init('...');
curl_setopt($cURLConnection, CURLOPT_POSTFIELDS, array(
'sign' => $sign,
'api_key' => $api_key
));
curl_setopt($cURLConnection, CURLOPT_HTTPHEADER, array(
'contentType: application/x-www-form-urlencoded',
'timestamp: '.$timeStamp,
'signature_method: HmacSHA256',//RSA
'echostr: '.$echostr
));
curl_setopt($cURLConnection, CURLOPT_RETURNTRANSFER, true);
$apiResponse = curl_exec($cURLConnection);
curl_close($cURLConnection);
var_dump($apiResponse);
function encode($data) {
return str_replace(['+', '/'], ['-', '_'], base64_encode($data));
}
function decode($data) {
return base64_decode(str_replace(['-', '_'], ['+', '/'], $data));
}
function GenerateDigest($requestPayload)
{
$utf8EncodedString = utf8_encode($requestPayload);
$digestEncode = hash("sha256", $utf8EncodedString, true);
return base64_encode($digestEncode);
}
function rsa_hash_sign($package, $privateKey) {
$rsa = new Crypt_RSA();
$rsa->setHash("sha256");
$rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);
$rsa->loadKey($privateKey);
$hash = hash('sha256', $package, true);
$signature = $rsa->sign($hash);
$hexData = bin2hex($signature);
$base64 = base64_encode($hexData);
return $base64;
}
function HmacSHA256_Sign($preparedStr, $secret)
{
signature result string which is signed by MD5 algorithm.
$hash = "";
$rsa = new Crypt_RSA();
$rsa->setHash('sha256');
$rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);
$rsa->loadKey($secret);
return $rsa->encrypt($secret);
}
But every $sign I try is giving me "Secret key does not exist" back as error. This is my first time trying anything with encryption and I am so lost and dont know how to solve this.

Different results in hmac encoding between Dart and PhP

I'm trying to encode a message in flutter and to verify it with some php code, but somehow there are some differences in the hmac encoding. The header and payload values are the same in both cases. But somehow there are some differences between the resulting values.
Any help would be very helpful, I'm stuck on this for some days now.
$base64UrlHeader = 'header';
$base64UrlPayload = 'payload';
$secret = 'secret';
$signature = hash_hmac('sha256', $base64UrlHeader . "." . $base64UrlPayload, $secret, true); // 9b491a7aa29955d9d67e302965665ba0cfa4306c00470f8946eb6aa67f676595
$base64UrlSignature = base64UrlEncode($signature); // sYql52zk6tqYeGSUsDv_219UtgpK3c8-TMuko4n_L5Q
function base64UrlEncode($text) {
return str_replace(
['+', '/', '='],
['-', '_', ''],
base64_encode($text)
);
}
And this is my dart code:
// This values are obtained by using the _base64UrlEncode method from below,
// I just wrote the values directly here not to clutter with code
var base64UrlHeader = 'header';
var base64UrlPayload = 'payload';
/// SIGNATURE
var secret = utf8.encode('secret');
var message = utf8.encode(base64UrlHeader + '.' + base64UrlPayload);
var hmac = new Hmac(sha256, secret);
var digest = hmac.convert(message); // b18aa5e76ce4eada98786494b03bffdb5f54b60a4addcf3e4ccba4a389ff2f94
var signature = _base64UrlEncode(digest.toString()) // YjE4YWE1ZTc2Y2U0ZWFkYTk4Nzg2NDk0YjAzYmZmZGI1ZjU0YjYwYTRhZGRjZjNlNGNjYmE0YTM4OWZmMmY5NA
// This method is used to obtain the header, payload and signature
static String _base64UrlEncode(String text) {
Codec<String, String> stringToBase64Url = utf8.fuse(base64url);
return stringToBase64Url
.encode(text)
.replaceAll('=', '')
.replaceAll('+', '-')
.replaceAll('/', '_');
}
Both the header and payload are obtained from the same json object,

How to perform HMAC-SHA1 with Base64 Encode?

I'm trying to setup an app to sign with my URLs so they may authenticate but I can't seem to figure out how to replicate the code that I'm trying from the following page: https://help.sendowl.com/help/signed-urls#example
order_id=12345&buyer_name=Test+Man&buyer_email=test%40test.com&product_id=123&signature=QpIEZjEmEMZV%2FHYtinoOj5bqAFw%3D
buyer_email=test#test.com&buyer_name=Test Man&order_id=12345&product_id=123
buyer_email=test#test.com&buyer_name=Test Man&order_id=12345&product_id=123&secret=t0ps3cr3t
publicStr&t0ps3cr3t
This is the steps:
First order the parameters (removing the signature) and unescape
them:
Next append your Signing secret:
Generate the key to sign with:
Perform the HMAC-SHA1 digest with Base 64 encode: QpIEZjEmEMZV/HYtinoOj5bqAFw=
The following is what I tried but end up not getting the same result:
$signKey = "t0ps3cr3t";
$signData = "buyer_email=test#test.com&buyer_name=Test Man&order_id=12345&product_id=123&secret=t0ps3cr3t";
$passData = hash_hmac("sha1", $signData, base64_decode(strtr($signKey)), true);
$passData = base64_encode($passData);
echo $passData;
I keep getting x8NXmAmkNBPYCXwtj65mdVJ8lPc=
I was able to replicate with the following: took me a bit to figure out something so simple.. been coding for 11 hours straight.
Thanks.
$data = "buyer_email=test#test.com&buyer_name=Test Man&order_id=12345&product_id=123&secret=t0ps3cr3t";
$key = "publicStr&t0ps3cr3t";
$pass1 = hash_hmac('sha1', $data, $key, true);
$pass = base64_encode($pass1);
echo $pass;
$pass will return "QpIEZjEmEMZV/HYtinoOj5bqAFw=", the correct value.
$current_timestamp = Carbon::now()->timestamp;
$signstring = "z001-line-anime-gif." . $current_timestamp . ".aaaabbbbccccdddd";
$secret = 'STG-7f*(:hsM-1_eQ_ZD175QgEoJhI$:oR.zEQ<z';
$sig = hash_hmac('sha1', $signstring, $secret);
$signature = hex2bin($sig);
$signature = base64_encode($signature);
return $signature;

Signature does not match in Amazon Web Services

I am writing a PHP code for AMAZON WEB SERVICES. This is my code.
<?php
function amazonEncode($text) {
$encodedText = "";
$j = strlen($text);
for ($i = 0; $i < $j; $i++) {
$c = substr($text, $i, 1);
if (!preg_match("/[A-Za-z0-9-_.~]/", $c)) {
$encodedText .= sprintf("%%%02X", ord($c));
} else {
$encodedText .= $c;
}
}
return $encodedText;
}
function amazonSign($url, $secretAccessKey) {
// 0. Append Timestamp parameter
$url .= "&Timestamp=" . gmdate("Y-m-dTH:i:sZ");
// 1a. Sort the UTF-8 query string components by parameter name
$urlParts = parse_url($url);
parse_str($urlParts["query"], $queryVars);
ksort($queryVars);
// 1b. URL encode the parameter name and values
$encodedVars = array();
foreach ($queryVars as $key => $value) {
$encodedVars[amazonEncode($key)] = amazonEncode($value);
}
// 1c. 1d. Reconstruct encoded query
$encodedQueryVars = array();
foreach ($encodedVars as $key => $value) {
$encodedQueryVars[] = $key . "=" . $value;
}
$encodedQuery = implode("&", $encodedQueryVars);
// 2. Create the string to sign
$stringToSign = "GET";
$stringToSign .= "n" . strtolower($urlParts["host"]);
$stringToSign .= "n" . $urlParts["path"];
$stringToSign .= "n" . $encodedQuery;
// 3. Calculate an RFC 2104-compliant HMAC with the string you just created,
// your Secret Access Key as the key, and SHA256 as the hash algorithm.
if (function_exists("hash_hmac")) {
$hmac = hash_hmac("sha256", $stringToSign, $secretAccessKey, TRUE);
} elseif (function_exists("mhash")) {
$hmac = mhash(MHASH_SHA256, $stringToSign, $secretAccessKey);
} else {
die("No hash function available!");
}
// 4. Convert the resulting value to base64
$hmacBase64 = base64_encode($hmac);
// 5. Use the resulting value as the value of the Signature request parameter
// (URL encoded as per step 1b)
$url .= "&Signature=" . amazonEncode($hmacBase64);
echo $url;
}
$url = 'http://webservices.amazon.com/onca/xml?Service=AWSECommerceService&AWSAccessKeyId=something&AssociateTag=something&Operation=ItemSearch&Keywords=Mustang&SearchIndex=Blended&Condition=Collectible&Timestamp=2016-08-08T12%3A00%3A00Z&Version=2013-08-01';
$SECRET_KEY = 'my_secret_key';
$url = amazonSign($url, $SECRET_KEY);
?>
This code returns me a URL. I use that URL inside my browser so that I can get my search results but using that URL gives me this error.
SignatureDoesNotMatchThe request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.
I am using these as AWSAccessKeyId and SECRET_KEY.
It's probably $stringToSign .= "n" should be $stringToSign .= "\n" but this might not be the only problem. If you use the official PHP SDK from Amazon instead of relying on custom scripts you'll have less issues.
The error you are seeing is usually a mistyped Access Key or Secret Access Key.
or the issue may be non-UTF-8 encoded string. Once i UTF-8 encoded it, the error will disappeared.
or if you sending a metadata with an empty value,than it will not work.
or if you not providing the Content-Length parameter than also such kind of issue can happen.

'Serialization of 'SimpleXMLElement' is not allowed when saving in Wordpress post_meta

I am working on an amazon affiliate wordpress page.
For that I am using the aws_signed_request function to get the price and link from amazon.
Here is the aws_signed_request function returning the xml:
function aws_signed_request($region, $params, $public_key, $private_key, $associate_tag) {
$method = "GET";
$host = "ecs.amazonaws.".$region;
$uri = "/onca/xml";
$params["Service"] = "AWSECommerceService";
$params["AWSAccessKeyId"] = $public_key;
$params["AssociateTag"] = $associate_tag;
$params["Timestamp"] = gmdate("Y-m-d\TH:i:s\Z");
$params["Version"] = "2009-03-31";
ksort($params);
$canonicalized_query = array();
foreach ($params as $param=>$value)
{
$param = str_replace("%7E", "~", rawurlencode($param));
$value = str_replace("%7E", "~", rawurlencode($value));
$canonicalized_query[] = $param."=".$value;
}
$canonicalized_query = implode("&", $canonicalized_query);
$string_to_sign = $method."\n".$host."\n".$uri."\n".
$canonicalized_query;
/* calculate the signature using HMAC, SHA256 and base64-encoding */
$signature = base64_encode(hash_hmac("sha256",
$string_to_sign, $private_key, True));
/* encode the signature for the request */
$signature = str_replace("%7E", "~", rawurlencode($signature));
/* create request */
$request = "http://".$host.$uri."?".$canonicalized_query."&Signature=".$signature;
/* I prefer using CURL */
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,$request);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
$xml_response = curl_exec($ch);
if ($xml_response === False)
{
return False;
}
else
{
$parsed_xml = #simplexml_load_string($xml_response);
return ($parsed_xml === False) ? False : $parsed_xml;
}
}
After that I get the asin from the post and generate the link and price
global $post;
$asin = get_post_meta($post->ID, 'ASIN', true);
$public_key = 'xxxxxxxxxxx';
$private_key = 'xxxxxxxxxxx';
$associate_tag = 'xxxxxxxxxxx';
$xml = aws_signed_Request('de',
array(
"MerchantId"=>"Amazon",
"Operation"=>"ItemLookup",
"ItemId"=>$asin,
"ResponseGroup"=>"Medium, Offers"),
$public_key,$private_key,$associate_tag);
$item = $xml->Items->Item;
$link = $item->DetailPageURL;
$price_amount = $item->OfferSummary->LowestNewPrice->Amount;
if ($price_amount > 0) {
$price_rund = $price_amount/100;
$price = number_format($price_rund, 2, ',', '.');
} else {
$price= "n.v.";
}
This all works pretty good when I echo the $link and $price. But I want to save the values in the custom fields of the wordpress post so I don't have to run the function every time.
update_post_meta($post->ID, 'Price', $price);
update_post_meta($post->ID, 'Link', $link);
This adds the price as the correct value, but when I want to add the link I get this error message:
Uncaught exception 'Exception' with message 'Serialization of
'SimpleXMLElement' is not allowed' in...
But when I remove the $parsed_xml=... function, it saves an empty value.
(Nearly) everything returned when you are traversing a SimpleXML object is actually another SimpleXML object. This is what lets you write $item->OfferSummary->LowestNewPrice->Amount: requesting ->OfferSummary on the $item object returns an object representing the OfferSummary XML node, so you can request ->LowestNewPrice on that object, and so on. Note that this applies to attributes too - $someNode['someAttribute'] will be an object, not a string!
In order to get the string content of an element or attribute, you have to "cast" it, using the syntax (string)$variable. Sometimes, PHP will know you meant to do this, and do it for you - for instance when using echo - but in general, it's good practice to always cast to string manually so that you won't have any surprises if you change your code later. You can also cast to an integer using (int), or a float using (float).
The second part of your problem is that SimpleXML objects are stored rather specially in memory, and can't be "serialized" (i.e. turned into a string that completely describes the object). This means that if you try to save them into a database or session, you will get the error you're seeing. If you actually wanted to save a whole block of XML, you could use $foo->asXML().
So, in short:
use $link = (string)$item->DetailPageURL; to get a string rather than an object
use update_post_meta($post->ID, 'ItemXML', $item->asXML()); if you ever want to store the whole item

Categories