I got an issue with HMAC.
I have to sign a form before sending it to a bank.
They only provide an example in PHP in their documentation.
I have a hex key to sign my data (e.g. FCEBA61A884A938E7E7FE4F5C68AA7F4A349768EE5957DDFBE99C1D05A09CBACF1FCF0A7084CB2E4CBA95193176C4395DE7F39EA9DBEBEF0907D77192AAE3E8A).
In the PHP exemple, they do this with the key before signing the data:
$key = "FCEBA61A884A938E7E7FE4F5C68AA7F4A349768EE5957DDFBE99C1D05A09CBACF1FCF0A7084CB2E4CBA95193176C4395DE7F39EA9DBEBEF0907D77192AAE3E8A";
$message = "param1=a¶m2=b";
$binKey = pack('H*', $key);
$signature = hash_hmac('sha512', $msg, $binKey);
echo $signature;
// => a3efb70368bee502ea57a1a4708cac8912a5172075ea8dec2de2770dfbb4c8fb587f03fdadc0ca4f9e1bb024cfda12866295b259f5fb4df2fe14d960874a68ab
I don't understand why they pack the key and if I should do something similar with my key.
I did the following in my Ruby code:
key = "FCEBA61A884A938E7E7FE4F5C68AA7F4A349768EE5957DDFBE99C1D05A09CBACF1FCF0A7084CB2E4CBA95193176C4395DE7F39EA9DBEBEF0907D77192AAE3E8A"
message = "param1=a¶m2=b"
digest = OpenSSL::Digest.new('sha512')
signature = OpenSSL::HMAC.hexdigest(digest, key, message)
puts signature
# => d817611845246640d1224a0874bf60fed0956a367aa3069b7947cbec56903bb5d8c54df170f5504c586dad55e4f879c70cf1a40526cfc9f35411195822c535ed
You need to do this in Ruby:
hash = OpenSSL::HMAC.hexdigest(digest, [key].pack('H*'), message)
The real issue here is that your PHP code uses two variable names for the message. You set $message, then use $msg, which means you're computing the hash for an undefined variable.
The packing of the hex representation of the key back into a binary form is the bit you're missing.
See this post for example: https://blog.bigbinary.com/2011/07/20/ruby-pack-unpack.html
You'll want something like this:
signature = OpenSSL::HMAC.hexdigest(digest, key.pack('H'), message)
I'm using this in my project:
bin_key = Array(keyTest).pack 'H*'
#hmac = OpenSSL::HMAC.hexdigest("SHA512", bin_key, msg).upcase
This works fine for me.
Related
In my pure Ruby app one of the components to create a token for my request authentication to an external API is to create signature which is HMAC value that is created using the api_key and the secret_key. The signature contains the following elements that are each separated by a new line \n (except the last line) and are in the same order as below list:
ts = '1529342939277'
nonce = '883b170c-a768-41a1-ae6d-c626323aa128'
host = 'ws.idms.lexisnexis.com'
resource_path = '/restws/identity/v3/accounts/11111/workflows/rdp.test.workflow/conversations'
body_hash = 'fQoIAs0IO4vNleZVE9tcI3Ni7h+niT+GrrgEHsKZOyM='
API_KEY = '6njQLkz7uCiz1ZeJ1bFCWX4DFVTfKQXa'
SECRET_KEY = 'CcdaZEt7co647iJoEc5G29CHtlo7T9M3'
# create string signature separated by new line
signature = [ts, nonce, host, resource_path, body_hash].join("\n")
# create HMAC for signature
mac = Base64.strict_encode64(OpenSSL::HMAC.hexdigest('SHA256', API_KEY, signature))
2.7.0 :146 > mac
=> "ZDE4NDQxZDdiNmZkODNiODgyODI4Nzc2OTQ3OGFlMjVhZTMyNThhZTZlMTRiMjkxMzI0NmQ5NzljNDJkZWVhZg=="
According to the docs the signature should be Syb6i+sRygAGCgxLQJ4NwwKcT5Mnkh4r3QXgwZ3vmcE= but I'm getting ZDE4NDQxZDdiNmZkODNiODgyODI4Nzc2OTQ3OGFlMjVhZTMyNThhZTZlMTRiMjkxMzI0NmQ5NzljNDJkZWVhZg== instead. Where did I go wrong?
I've got an example how to do it in Java if something will be unclear: https://gist.github.com/mrmuscle1234/20c9d46d163fee66528449c0ea8419a7
I've just caught a crash reported on sentry, I am trying to debug and see the root cause for the problem.
Luckily, in the cookies panel, I can see the laravel_session value that was used while crash happened.
The question, is, how can decrypt the cookie?
You can decrypt the cookie with the following code:
$cookie = 'eyJpdiI6ImFUQ0FvMWFSVlNvTmhlQjdLWGw1Z1E9PSIsInZhbHVlIjoicFh6Q09iTDl0K0huWU1Nc1NYVmxSY2hPRGU5Vk85dDJyYUpRbUVjRWg5R0JxYkVobkF3YkZVcVQrakFFUmxaVnZrTjFST3F3RTZ4akpDZEpvUFJiQXc9PSIsIm1hYyI6IjlhYmJhMTY3MWMxYWI3YjJmNmFjMmNkZWE0MWZmMmVhNTNiMjI5ZWY3NzUwNzQ0ZjAzMGQ1ZGU0YzVhNjJmZGYifQ==';
$cookie_contents = json_decode( base64_decode( $cookie, true ));
$value = base64_decode( $cookie_contents->value );
$iv = base64_decode( $cookie_contents->iv );
$clear = unserialize( \openssl_decrypt($value, \Config::get( 'app.cipher' ), \Config::get( 'app.key' ), OPENSSL_RAW_DATA, $iv));
echo "Cookie contents (Session ID): $clear\n";
You should end up with a session ID that looks something like this:
Laravel 5.1: 55782b00dbfcc3f848585ac2cefc66802d773cf5
Laravel 5.4: yPjeV74joY4MtMNNtTpeOYBP2CMixJBBChc9HRND
I didn't test with Laravel 5.3, but I'm confident it will work.
When using this code, make sure you paste the entire contents of the cookie into the $cookie variable, including the two equals signs at the end.
For laravel 6 I think it's pretty much the same
$base64_key = "base64:ISAcSPwQ0HDqqLygaS9LyPzs5ZujMAKOjBou+gyz9sw=";
$payload = json_decode(base64_decode($_COOKIE["your_cookie_name"]), true);
$iv = base64_decode($payload['iv']);
$key = base64_decode(substr($base64_key, 7));
$sessionId = openssl_decrypt($payload['value'], 'AES-256-CBC', $key, 0, $iv);
echo "Session Id: $sessionId";
But check few things:
Cipher encoding, mine is 'AES-256-CBC', it can be 'AES-128-CBC' if your key length is 16
Key format, mine start with "base64:" so I have to remove this part first
I have ruby 1.8.7 code that create hmac with sha1
key= '123'
digest = Digest::SHA1.new
digest << 'test string'
digest << key
result = digest.hexdigest
# "c1bdfd602e1581f1ab91928e2c3fd371a1e63a5c"
I want to replicate this with node.js:
key= '123';
myhmac = crypto.createHmac('sha1', key);
result = myhmac.update('test string').digest('hex');
// 'a145f4d366e9e4e96b80bc427144ba77b3c7151a'
But the result is different.
What should I do in nodejs to have the same result as from ruby?
You are comparing a plain SHA1 digest in your Ruby code with a HMAC (using SHA1 as its hash function) in your Node code. These are different things, although the HMAC makes use of SHA1.
Usually you would want to use the HMAC over the plain SHA1. To do that in Ruby you could do something like:
require 'openssl'
key = '123'
data = 'test string'
digest = OpenSSL::Digest::SHA1.new
# See how HMAC uses SHA1 here:
result = OpenSSL::HMAC.hexdigest(digest, key, data)
# => "a145f4d366e9e4e96b80bc427144ba77b3c7151a", same as your node result
To reproduce your Ruby results in Node (calculating the SHA1 of the message + key), you want something like this:
const crypto = require('crypto');
const hash = crypto.createHash('sha1'); // Just SHA1, no HMAC
hash.update('test string');
hash.update('123'); // The Ruby code is hashing the concatenation of
// the data and key
result = hash.digest('hex');
// => 'c1bdfd602e1581f1ab91928e2c3fd371a1e63a5c', same as Ruby code
To make it works for both (nodejs and ruby), please make sure :
(important) make it in the same format.
ruby:
payload = "{'name': 'james kachiro sarumaha', 'data': [], 'is_available': true}"
payload = payload.to_json
nodejs:
payload = "{'name': 'james kachiro sarumaha', 'data': [], 'is_available': true}"
payload = JSON.stringify(payload)
have a correct key. (32)
ruby:
key = "v1tg3cOvfNdxh4TXxtdVmeB106doeQFS"
nodejs:
key = "v1tg3cOvfNdxh4TXxtdVmeB106doeQFS"
conversion
ruby :
digest = OpenSSL::Digest::SHA256.new
result = OpenSSL::HMAC.hexdigest(digest, password, payload)
#bd0724a05fab03e64e0112d09ceb11b6c1cbd8f9629a1d303e8d395d36cce396
nodejs:
crypto.createHmac('sha256', key).update(payload).digest('hex')
//bd0724a05fab03e64e0112d09ceb11b6c1cbd8f9629a1d303e8d395d36cce396
crypto from crypto module (nodejs core).
in my case, I'm using SHA256 but you can change it to SHA1. it will have the same result also.
tx
I decode (secret_key,client_id, path) into signature by following code :
require 'rubygems'
require 'base64'
require 'cgi'
require 'hmac-sha1'
#client_id = "asdkasdlda"
#secret = "3fdsdsfxds"
binary_key = Base64.decode64(#secret)
params.update({"client" => #client_id})
path = uri_path + "?" + params.collect{|k,v| "#{k}=#{v}"}.inject{|initial,cur| initial + "&" + cur}
digest = HMAC::SHA1.new(binary_key).update(path).digest
digest = Base64.encode64(digest).gsub(/[+\/]/, {"+" => "-", "/" => "_"}).delete("=")
return "#{path}&sig=#{digest}"
So, this code generates sig and path. we send request to server in following way:
/api/v1/customers/sign_in.json?user[email]=amit1656789#gmail.com&user[password]=[FILTERED]&client=asdkasdlda&sig=JSdP5xUHhgS8ZbKApBOIlsJKg_Q
Now, on server side, i want to decode this params["sign"] into app_id, secret_key and path means reverse process of above code. But i am not found any reverse process of this. Means
(app_id, secret, path) => "signature"
"signature" => (app_id, secret, path) /* Here i stuck */
First thing you should know:
"signature" => (app_id, secret, path)
This is not possible. It is not how MACs of any kind work. The signature does not contain the data. Signatures are meant to be sent alongside the message that they sign.
For secure HMAC, you should never send the secret with the message that you sign. It is also not possible to figure out a secret from the signature, except by repeatedly guessing what the value might be.
The usual way to confirm a signature is to follow the same process on the server, signing the same message, using the same secret (which the server should already have), and compare the signatures. You have made it difficult for yourself because you have signed the params as you sent them, and then put the signature on the end. You have to re-construct the message.
First, you need to use whatever web server library you can to get the request URI including the query string
signed_uri = "/api/v1/customers/sign_in.json?user[email]=amit1656789#gmail.com&user[password]=[FILTERED]&client=asdkasdlda&sig=JSdP5xUHhgS8ZbKApBOIlsJKg_Q"
Then split it into the message and its signature (I'll leave that to you, but just a regular expression ought to work):
message = "/api/v1/customers/sign_in.json?user[email]=amit1656789#gmail.com&user[password]=[FILTERED]&client=asdkasdlda"
signature = "JSdP5xUHhgS8ZbKApBOIlsJKg_Q"
To decode this signature back to the original digest (for easy comparison), just reverse the replace and encoding you did at the end on the client:
client_digest = Base64.decode64(
signature.gsub(/[-_]/, {"-" => "+", "_" => "/"}) )
Then on the server (where you should already have a value for #secret), calculate what you expect the signature to be:
#secret = '3fdsdsfxds'
binary_key = Base64.decode64(#secret)
server_digest = HMAC::SHA1.new(binary_key).update( message ).digest
if server_digest == client_digest
puts "The message was signed correctly"
else
puts "ERROR: The message or signature is not correct!"
end
I was going through the Amazon Product Advertising API REST signature docs and I got stuck at #8
Calculate an RFC 2104-compliant HMAC with the SHA256 hash algorithm using the string above with our "dummy" Secret Access Key: 1234567890. For more information about this step, see documentation and code samples for your programming language.
I managed to get it on one more try with the help of Calculating a SHA hash with a string + secret key in python.
The following creates the correct signature:
require 'openssl'
secret_key = '1234567890'
query = 'AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&ItemId=0679722769&Operation=ItemLookup&ResponseGroup=ItemAttributes%2COffers%2CImages%2CReviews&Service=AWSECommerceService&Timestamp=2009-01-01T12%3A00%3A00Z&Version=2009-01-06'
data = ['GET', 'ecs.amazonaws.com', '/onca/xml', query].join("\n")
sha256 = OpenSSL::Digest::SHA256.new
sig = OpenSSL::HMAC.digest(sha256, secret_key, data)
signature = Base64.encode64(sig)
Adding to AJcodez answer:
I would do:
...
signature_raw = Base64.strict_encode64(sig)
signature = CGI::escape(signature_raw)
encode64adds a newline at the end, strict_encode64() does not.
https://stackoverflow.com/a/2621023/2760406
Amazon wants you to "URL encode the plus (+) and equal (=) characters in the signature" #9 - won't work now if you don't.
http://docs.aws.amazon.com/AWSECommerceService/latest/DG/rest-signature.html#rest_detailedexample
You can calculate a keyed-hash message authentication code (HMAC-SHA256) signature with your secret access key by using cryptoJs
First install cryptoJs locally in your system by typing
npm install crypto-js
to install it globally you node a flag -g to the above command. Then add this code and run it.
var CryptoJS = require("crypto-js");
// Calculate an RFC 2104-compliant HMAC with the SHA256 hash algorithm
var exampleString =
"GET\n" +
"webservices.amazon.com\n" +
"/onca/xml\n" +
"AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&AssociateTag=mytag-20&ItemId=0679722769&Operation=ItemLookup&ResponseGroup=Images%2CItemAttributes%2COffers%2CReviews&Service=AWSECommerceService&Timestamp=2014-08-18T12%3A00%3A00Z&Version=2013-08-01";
var signature = CryptoJS.HmacSHA256(exampleString, "1234567890");
console.log("test signature", signature.toString(CryptoJS.enc.Base64));