I build Android app with billing-in app(version 3). I want verify purchase in my server PHP with openssl_verify().
I neet four values:
$data, $signature, $public_key and $SIGNATURE_ALGORITHM. I found a solution here, but it not understand to me in what form should be $data?
I get responseData in Android app:
'{
"orderId":"12999763169054705758.1371079406387615",
"packageName":"com.example.app",
"productId":"exampleSku",
"purchaseTime":1345678900000,
"purchaseState":0,
"developerPayload":"bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
"purchaseToken":"rojeslcdyyiapnqcynkjyyjh"
}'
I have signature from app.
How do I need to convert string so I can use it in the function php openssl_verify()?
Thanks.
I use in my project
public function verifySignatureTransaction($signed_data, $signature, $public_key_base64) {
$key = "-----BEGIN PUBLIC KEY-----\n" .
chunk_split($public_key_base64, 64, "\n") .
'-----END PUBLIC KEY-----';
//using PHP to create an RSA key
$key = openssl_pkey_get_public($key);
if ($key === false) {
throw new \InvalidArgumentException("Public key not valid");
}
$signature = base64_decode($signature);
//using PHP's native support to verify the signature
$result = openssl_verify(
$signed_data,
$signature,
$key,
OPENSSL_ALGO_SHA1
);
if (0 === $result) {
return false;
} else {
if (1 !== $result) {
return false;
} else {
return true;
}
}
}
And $signed_data its json string without formatting
Related
When a client installs the app, they have the option to click on the app name in the list of apps on the /admin/apps page.
When they click that page, my PHP index file for my app receives these $_GET vars:
hmac = some_long_alphanumaeric_hmac
locale = en
protocol = https://
shop = example-shop.myshopify.com
timestamp = 1535609063
To verify a webhook from Shopify, I successfully use this:
function verify_webhook($data, $hmac_header, $app_api_secret) {
$calculated_hmac = base64_encode(hash_hmac('sha256', $data, $app_api_secret, true));
return ($hmac_header == $calculated_hmac);
}
// Set vars for Shopify webhook verification
$hmac_header = $_SERVER['HTTP_X_SHOPIFY_HMAC_SHA256'];
$data = file_get_contents('php://input');
$verified = verify_webhook($data, $hmac_header, MY_APP_API_SECRET);
Is it possible to verify an app admin page visit is from a Shopify client that has the app installed?
PS: I've looked through both, the Embedded Apps API (but I can't figure out if that's even the right documentation or if I'm doing something wrong), as well as the GitHub example provided (which has no instructions on how to verify an Embedded App admin page visit).
UPDATE:
I've tried various other ways, discovering some ridiculous problems along the way, but still no luck.
The method I understand should be used to verify a Shopify HMAC is something akin to this:
function verify_hmac($hmac = NULL, $shopify_app_api_secret) {
$params_array = array();
$hmac = $hmac ? $hmac : $_GET['hmac'];
unset($_GET['hmac']);
foreach($_GET as $key => $value){
$key = str_replace("%","%25",$key);
$key = str_replace("&","%26",$key);
$key = str_replace("=","%3D",$key);
$value = str_replace("%","%25",$value);
$value = str_replace("&","%26",$value);
$params_array[] = $key . "=" . $value;
}
$params_string = join('&', $params_array);
$computed_hmac = hash_hmac('sha256', $params_string, $shopify_app_api_secret);
return hash_equals($hmac, $computed_hmac);
}
But the line $params_string = join('&', $params_array); causes an annoying problem by encoding ×tamp as xtamp ... Using http_build_query($params_array) results in the same ridiculous thing. Found others having this same problem here. Basically resolved by encoding the & as &, to arrive at $params_string = join('&', $params_array);.
My final version is like this, but still doesn't work (all the commented code is what else I've tried to no avail):
function verify_hmac($hmac = NULL, $shopify_app_api_secret) {
$params_array = array();
$hmac = $hmac ? $hmac : $_GET['hmac'];
unset($_GET['hmac']);
// unset($_GET['protocol']);
// unset($_GET['locale']);
foreach($_GET as $key => $value){
$key = str_replace("%","%25",$key);
$key = str_replace("&","%26",$key);
$key = str_replace("=","%3D",$key);
$value = str_replace("%","%25",$value);
$value = str_replace("&","%26",$value);
$params_array[] = $key . "=" . $value;
// This commented out method below was an attempt to see if
// the imporperly encoded query param characters were causing issues
/*
if (!isset($params_string) || empty($params_string)) {
$params_string = $key . "=" . $value;
}
else {
$params_string = $params_string . "&" . $key . "=" . $value;
}
*/
}
// $params_string = join('&', $params_array);
// echo $params_string;
// $computed_hmac = base64_encode(hash_hmac('sha256', $params_string, $shopify_app_api_secret, true));
// $computed_hmac = base64_encode(hash_hmac('sha256', $params_string, $shopify_app_api_secret, false));
// $computed_hmac = hash_hmac('sha256', $params_string, $shopify_app_api_secret, false);
// $computed_hmac = hash_hmac('sha256', $params_string, $shopify_app_api_secret, true);
$computed_hmac = hash_hmac('sha256', http_build_query($params_array), $shopify_app_api_secret);
return hash_equals($hmac, $computed_hmac);
}
If you get a hit from Shopify, the first thing you do is check in your persistence layer if you have the shop registered. If you do, and you have a session of some kind setup, you are free to render your App to that shop. If you do not have the shop persisted, you go through the oAuth cycle to get an authentication token to use on the shop, which you persist along with the shop and new session.
For any routes or end points in your shop where you are receiving webhooks, of course those requests have no session, so you use the HMAC security approach to figure out what to do. So your question is clearly straddling two different concepts, each handled differently. The documentation is pretty clear on the differences.
Here is the relevant documentation: https://shopify.dev/tutorials/authenticate-with-oauth#verification. This info by Sandeep was also very helpful too: https://community.shopify.com/c/Shopify-APIs-SDKs/HMAC-verify-app-install-request-using-php/m-p/140097#comment-253000.
Here is what worked for me:
function verify_visiter() // returns true or false
{
// check that timestamp is recent to ensure that this is not a 'replay' of a request that has been intercepted previously (man in the middle attack)
if (!isset($_GET['timestamp'])) return false;
$seconds_in_a_day = 24 * 60 * 60;
$older_than_a_day = $_GET['timestamp'] < (time() - $seconds_in_a_day);
if ($older_than_a_day) return false;
$shared_secret = Your_Shopify_app_shared_secret;
$hmac_header = $_GET['hmac'];
unset($_GET['hmac']);
$data = urldecode(http_build_query($_GET));
$calculated_hmac = hash_hmac('sha256', $data, $shared_secret, false);
return hash_equals($hmac_header, $calculated_hmac);
}
$verified = verify_visiter();
if (!$verified) {
exit('User verification failed.');
}
// ... everything else...
public function authenticateCalls($data = NULL, $bypassTimeCheck = FALSE)
{
$da = array();
foreach($data as $key => $val)
{
$da[$key] = $val;
}
if(isset($da['hmac']))
{
unset($da['hmac']);
}
ksort($da);
// Timestamp check; 1 hour tolerance
if (!$bypassTimeCheck)
{
if (($da['timestamp'] - time() > 3600))
{
return false;
}
}
// HMAC Validation
$queryString = http_build_query($da);
$match = $data['hmac'];
$calculated = hash_hmac('sha256', $queryString, $this->_API['API_SECRET']);
return $calculated === $match;
}
This is my code :
function verifyRequest($request, $secret) {
// Per the Shopify docs:
// Everything except hmac and signature...
$hmac = $request['hmac'];
unset($request['hmac']);
unset($request['signature']);
// Sorted lexilogically...
ksort($request);
// Special characters replaced...
foreach ($request as $k => $val) {
$k = str_replace('%', '%25', $k);
$k = str_replace('&', '%26', $k);
$k = str_replace('=', '%3D', $k);
$val = str_replace('%', '%25', $val);
$val = str_replace('&', '%26', $val);
$params[$k] = $val;
}
echo $http = "protocol=". urldecode("https://").http_build_query( $params) ;
echo $test = hash_hmac("sha256", $http , $secret);
// enter code hereVerified when equal
return $hmac === $test;
}
The hmac from shopi and hmac created from my code is not matching.
What am I doing wrong?
You only need to include the request parameters when creating the list of key-value pairs - don't need "protocol=https://".
https://help.shopify.com/api/getting-started/authentication/oauth#verification
You'll need to urldecode() the result of http_build_query(). It returns a url-encoded query string.
http://php.net/manual/en/function.http-build-query.php
Instead of:
echo $http = "protocol=". urldecode("https://").http_build_query( $params) ;
echo $test = hash_hmac("sha256", $http , $secret);
Something like this:
$http = urldecode(http_build_query($params));
$test = hash_hmac('sha256', $http, $secret);
hmac can be calculated in any programming language using sha256 cryptographic algorithm.
However the doc for hmac verification is provided by shopify but still there is confusion among app developers how to implement it correctly.
Here is the code in php for hmac verification.
Ref. http://code.codify.club
<?php
function verifyHmac()
{
$ar= [];
$hmac = $_GET['hmac'];
unset($_GET['hmac']);
foreach($_GET as $key=>$value){
$key=str_replace("%","%25",$key);
$key=str_replace("&","%26",$key);
$key=str_replace("=","%3D",$key);
$value=str_replace("%","%25",$value);
$value=str_replace("&","%26",$value);
$ar[] = $key."=".$value;
}
$str = join('&',$ar);
$ver_hmac = hash_hmac('sha256',$str,"YOUR-APP-SECRET-KEY",false);
if($ver_hmac==$hmac)
{
echo 'hmac verified';
}
}
?>
Notice for other requests like the App Proxy a HMAC will not be preset, so you'll need to calculate the signature. Here a function that caters for both types of requests including webhooks:
public function authorize(Request $request)
{
if( isset($request['hmac']) || isset($request['signature']) ){
try {
$signature = $request->except(['hmac', 'signature']);
ksort($signature);
foreach ($signature as $k => $val) {
$k = str_replace('%', '%25', $k);
$k = str_replace('&', '%26', $k);
$k = str_replace('=', '%3D', $k);
$val = str_replace('%', '%25', $val);
$val = str_replace('&', '%26', $val);
$signature[$k] = $val;
}
if(isset($request['hmac'])){
$test = hash_hmac('sha256', http_build_query($signature), env('SHOPIFY_API_SECRET'));
if($request->input('hmac') === $test){
return true;
}
} elseif(isset($request['signature'])){
$test = hash_hmac('sha256', str_replace('&', '', urldecode(http_build_query($signature))), env('SHOPIFY_API_SECRET'));
if($request->input('signature') === $test){
return true;
}
}
} catch (Exception $e) {
Bugsnag::notifyException($e);
}
} else { // If webhook
$calculated_hmac = base64_encode(hash_hmac('sha256', $request->getContent(), env('SHOPIFY_API_SECRET'), true));
return hash_equals($request->server('HTTP_X_SHOPIFY_HMAC_SHA256'), $calculated_hmac);
}
return false;
}
The above example uses some Laravel functions, so уоu may want to replace them if you use a different framework.
I've got the following to do this:
// Remove the 'hmac' parameter from the query string
$query_string_rebuilt = removeParamFromQueryString($_SERVER['QUERY_STRING'], 'hmac');
// Check the HMAC
if(!checkHMAC($_GET['hmac'], $query_string_rebuilt, $shopify_api_secret_key)) {
// Error code here
}
/**
* #param string $comparison_data
* #param string $data
* #param string $key
* #param string $algorithm
* #param bool $binary
* #return bool
*/
function checkHMAC($comparison_data, $data, $key, $algorithm = 'sha256', $binary=false) {
// Check the HMAC
$hash_hmac = hash_hmac($algorithm, $data, $key, $binary);
// Return true if there's a match
if($hash_hmac === $comparison_data) {
return true;
}
return false;
}
/**
* #param string $query_string
* #param string $param_to_remove
* #return string
*/
function removeParamFromQueryString(string $query_string, string $param_to_remove) {
parse_str($query_string, $query_string_into_array);
unset($query_string_into_array[$param_to_remove]);
return http_build_query($query_string_into_array);
}
I try to implement Crypt::encrypt function in php and this code is here:
$key = "ygXa6pBJOWSAClY/J6SSVTjvJpMIiPAENiTMjBrcOGw=";
$iv = random_bytes(16);
$value = \openssl_encrypt(serialize('123456'), 'AES-256-CBC', $key, 0, $iv);
bIv = base64_encode($iv);
$mac = hash_hmac('sha256', $bIv.$value, $key);
$c = ['iv'=>$bIv,'value'=>$value,'mac'=>$mac];
$json = json_encode($c);
$b = base64_encode($json);
But result is wrong.
I am thinking i should do something on $key before set in openssl_encrypt function.
Please help.
Thank you.
SOLVED:
We can implement this method like this:
$text = '123456';
$key = "ygXa6pBJOWSAClY/CFEdOTjvJpMIiPAMQiTMjBrcOGw=";
$key = (string)base64_decode($key);
$iv = random_bytes(16);
$value = \openssl_encrypt(serialize($text), 'AES-256-CBC', $key, 0, $iv);
$bIv = base64_encode($iv);
$mac = hash_hmac('sha256', $bIv.$value, $key);
$c_arr = ['iv'=>$bIv,'value'=>$value,'mac'=>$mac];
$json = json_encode($c_arr);
$crypted = base64_encode($json);
echo $crypted;
This work tor me.
enjoy :)
Be Successful
Here is the implementation, directly from the official source code.
public function encrypt($value)
{
$iv = random_bytes(16);
$value = \openssl_encrypt(serialize($value), $this->cipher, $this->key, 0, $iv);
if ($value === false) {
throw new EncryptException('Could not encrypt the data.');
}
// Once we have the encrypted value we will go ahead base64_encode the input
// vector and create the MAC for the encrypted value so we can verify its
// authenticity. Then, we'll JSON encode the data in a "payload" array.
$mac = $this->hash($iv = base64_encode($iv), $value);
$json = json_encode(compact('iv', 'value', 'mac'));
if (! is_string($json)) {
throw new EncryptException('Could not encrypt the data.');
}
return base64_encode($json);
}
$iv should be the same as in the source
$this->key is the encryption key you set in your .env file, encoded in b64
$this->cipher should be the one you configured in your laravel configurations and compatible to your key-length.
In your example, you have set your $key to the value after the "base64:"-string, which is not the key. You need to encode the key with base64 before passing it.
So the the $key to the base64 encode of ygXa6pBJOWSAClY/J6SSVTjvJpMIiPAENiTMjBrcOGw=, which is eWdYYTZwQkpPV1NBQ2xZL0o2U1NWVGp2SnBNSWlQQUVOaVRNakJyY09Hdz0K
I want to encrypt/decrypt files by chunks, because file size can be quite large (50-100Mb). I found code of encryption class on stack overflow and changed it just a bit:
class filecrypt{
var $_CHUNK_SIZE;
var $_CHIPHER;
var $_MODE;
function __construct($chipher, $mode){
$this->_CHUNK_SIZE = 100*1024; // 100Kb
$this->_CHIPHER = $chipher;
$this->_MODE = $mode;
}
public function setChunkSize($value)
{
$this->_CHUNK_SIZE = $value;
}
public function encrypt($string, $key, $vector){
$key = pack('H*', $key);
if (extension_loaded('mcrypt') === true) return mcrypt_encrypt($this->_CHIPHER, substr($key, 0, mcrypt_get_key_size($this->_CHIPHER, $this->_MODE)), $string, $this->_MODE, $vector);
return false;
}
public function decrypt($string, $key, $vector){
$key = pack('H*', $key);
if (extension_loaded('mcrypt') === true) return mcrypt_decrypt($this->_CHIPHER, substr($key, 0, mcrypt_get_key_size($this->_CHIPHER, $this->_MODE)), $string, $this->_MODE, $vector);
return false;
}
public function encryptFileChunks($source, $destination, $key, $vector){
return $this->cryptFileChunks($source, $destination, $key, 'encrypt', $vector);
}
public function decryptFileChunks($source, $destination, $key, $vector){
return $this->cryptFileChunks($source, $destination, $key, 'decrypt', $vector);
}
private function cryptFileChunks($source, $destination, $key, $op, $vector){
if($op != "encrypt" and $op != "decrypt") return false;
$buffer = '';
$inHandle = fopen($source, 'rb');
$outHandle = fopen($destination, 'wb+');
if ($inHandle === false) return false;
if ($outHandle === false) return false;
while(!feof($inHandle)){
$buffer = fread($inHandle, $this->_CHUNK_SIZE);
if($op == "encrypt") $buffer = $this->encrypt($buffer, $key, $vector);
elseif($op == "decrypt") $buffer = $this->decrypt($buffer, $key, $vector);
fwrite($outHandle, $buffer);
}
fclose($inHandle);
fclose($outHandle);
return true;
}
public function printFileChunks($source, $key, $vector){
$buffer = '';
$inHandle = fopen($source, 'rb');
if ($inHandle === false) return false;
while(!feof($inHandle)){
$buffer = fread($inHandle, $this->_CHUNK_SIZE);
$buffer = $this->decrypt($buffer, $key, $vector);
echo $buffer;
}
return fclose($inHandle);
}
}
So, I tested this class in my script:
$chipher = MCRYPT_RIJNDAEL_128;
$mode = MCRYPT_MODE_CFB;
$filecrypt = new filecrypt($chipher, $mode);
$key = '3da541559918a808c2402bba5012f6c60b27661c'; // Your encryption key
$vectorSize = mcrypt_get_iv_size($chipher, $mode);
$vector = mcrypt_create_iv($vectorSize, MCRYPT_DEV_URANDOM);
//Encrypt file
$filecrypt->setChunkSize(8*1024);
$filecrypt->encryptFileChunks(APPLICATION_PATH.'/../data/resources/img1.jpg', APPLICATION_PATH.'/../data/resources/img1_en.jpg', $key, $vector);
//Decrypt file
$filecrypt->setChunkSize(8*1024);
$filecrypt->decryptFileChunks(APPLICATION_PATH.'/../data/resources/img1_en.jpg', APPLICATION_PATH.'/../data/resources/img1_res.jpg', $key, $vector);
Everything works fine and restores image is absolutely the same as source image.
BUT if I set different chunk size for encryption and decryption processes, restored image will be corrupted. This is source image:
This is restored image after encryption/decryption with different chunk size:
Here is a code:
$chipher = MCRYPT_RIJNDAEL_128;
$mode = MCRYPT_MODE_CFB;
$filecrypt = new filecrypt($chipher, $mode);
$key = '3da541559918a808c2402bba5012f6c60b27661c'; // Your encryption key
$vectorSize = mcrypt_get_iv_size($chipher, $mode);
$vector = mcrypt_create_iv($vectorSize, MCRYPT_DEV_URANDOM);
//Encrypt file
$filecrypt->setChunkSize(8*1024);
$filecrypt->encryptFileChunks(APPLICATION_PATH.'/../data/resources/img1.jpg', APPLICATION_PATH.'/../data/resources/img1_en.jpg', $key, $vector);
//Decrypt file
$filecrypt->setChunkSize(4*1024);
$filecrypt->decryptFileChunks(APPLICATION_PATH.'/../data/resources/img1_en.jpg', APPLICATION_PATH.'/../data/resources/img1_res.jpg', $key, $vector);
My question is how does chunk size influence on encryption/decryption process? Does it connected with block cipher mode and paddings? How can I encrypt data by chunks?
Maybe I should use another cipher for this purpose?
Does it connected with block cipher mode and paddings?
Spot on.
Maybe I should use another cipher for this purpose?
You could do that and use a stream-like cipher mode such as CTR, which doesn't require padding.
OR you could take care of the chunk sizes that you pick. It should be divisible by whatever mcrypt_get_block_size() returns for your cipher.
Basically, block cipher modes will split your data into "chunks" too, only those would be called blocks - hence, block size. Whenever the condition $dataSize % $blockSize !== 0 is true, the last block will be padded (by MCrypt) with NUL-bytes.
Then, during the decryption phase, MCrypt doesn't trim these NUL-bytes, and your image gets corrupted.
I'm trying to build a valid signature key (I'm using the HMAC-SHA1 method), so far this is still invalid(i'm using an online test server at http://term.ie/oauth/example/client.php):
function _build_signature_hmac($base_url, $params, $consumer_key, $token_secret = '')
{
// Setup base-signature data
$data = 'POST&' . $base_url . '&';
// Sort the params array keys first
ksort($params);
// Attach params string
$data .= rawurlencode(http_build_query($params));
// Build the signature key
$key = rawurlencode($consumer_key) . '&' . rawurlencode($token_secret);
return base64_encode(hash_hmac('sha1', $data, $key));
}
Since this is a request for an unauthorized token, the $token_secret string is empty.
The returned signature looks like this:
POST&http://term.ie/oauth/example/request_token.php&oauth_consumer_key%3Dkey%26oauth_nonce%3D0uPOn3pPUbPlzWx2cO6citRPafIni5%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1298745681%26oauth_version%3D1.0
And the $key looks like this: secret&
The keys/secrets are all correct and I'm getting a response from the server saying 'invalid signature'. Am I building it the right way?
The method from the implementation I am using....
public function build_signature($request, $consumer, $token) {
$base_string = $request->get_signature_base_string();
$request->base_string = $base_string;
$key_parts = array(
$consumer->secret,
($token) ? $token->secret : ""
);
$key_parts = OAuthUtil::urlencode_rfc3986($key_parts);
$key = implode('&', $key_parts);
return base64_encode(hash_hmac('sha1', $base_string, $key, true));
}
public static function urlencode_rfc3986($input) {
if (is_array($input)) {
return array_map(array('OAuthUtil', 'urlencode_rfc3986'), $input);
} else if (is_scalar($input)) {
return str_replace(
'+',
' ',
str_replace('%7E', '~', rawurlencode($input))
);
} else {
return '';
}
If it helps at all...