AWS signature version 4 signing key issue ruby - ruby-on-rails

Has to be done in ruby
my code is:
require "openssl"
require "time"
require "uri"
require "pathname"
def getSignatureKey (key, dateStamp, regionName, serviceName)
digest= OpenSSL::Digest.new('sha256')
kDate = OpenSSL::HMAC.hexdigest(digest, ("AWS4" + key), dateStamp)
puts("kdate: #{kDate}")
kRegion = OpenSSL::HMAC.hexdigest(digest, kDate, regionName)
puts("kRegion: #{kRegion}")
kService = OpenSSL::HMAC.hexdigest(digest, kRegion, serviceName)
puts("kService: #{kService}")
kSigning = OpenSSL::HMAC.hexdigest(digest, kService, "aws4_request")
puts("kSigning: #{kSigning}")
kSigning
end
key = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY'
dateStamp = '20120215'
regionName = 'us-east-1'
serviceName = 'iam'
puts("===========================================OUTPUT IS: ==============================================")
getSignatureKey(key,dateStamp,regionName,serviceName)
which produces the output:
kdate: 969fbb94feb542b71ede6f87fe4d5fa29c789342b0f407474670f0c2489e0a0d
kRegion:a59e30f9d899c47b3dd68ea1c0ab3bb529e03a8f4ed2f54cb64af547330a22a0
kService:e33cbcda009243ed24c04af249a477d33b33c97ba7717327636afab2c6e6ef78
kSigning:9d8a8749deaee911a4ed0d04fb34af903f690d0423c146023bd282ba8ebf955b
however the output should be, according to aws documentation: https://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html):
kdate: 969fbb94feb542b71ede6f87fe4d5fa29c789342b0f407474670f0c2489e0a0d
kRegion:69daa0209cd9c5ff5c8ced464a696fd4252e981430b10e3d3fd8e2f197d7a70c
kService:f72cfd46f26bc4643f06a11eabb6c0ba18780c19a8da0c31ace671265e3c87fa
kSigning:f4780e2d9f65fa895f9c67b32ce1baf0b0d8a43505a000a1a9e090d414db404d
so the kdate matches but the others do not, I haven't miss-spelled "us-east-1" so I'm really not sure why they don't match. If anyone could offer advice I'd be very grateful.

Related

Manually encrypting and decrypting cookie - Rails

I'm new to rails and trying to encrypt and decrypt a cookie manually. Can anyone tell me where I'm going wrong?
The below code raises this error ActiveSupport::MessageVerifier::InvalidSignature
require 'cgi'
require 'json'
require 'active_support'
def verify_and_decrypt_session_cookie(cookie, secret_key_base)
cookie = CGI::unescape(cookie)
salt = 'encrypted cookie'
signed_salt = 'signed encrypted cookie'
key_generator = ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)
secret = key_generator.generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len]
sign_secret = key_generator.generate_key(signed_salt)
encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON)
encryptor.decrypt_and_verify(cookie)
end
cookie = 'example123'
secret = 'Rails.application.secret_key_base'
p verify_and_decrypt_session_cookie(cookie, secret)
Thanks

How to verify a webhook with Ruby?

I'm using Ruby 2.5.5 and Rails 5.2. I'm trying to verify a Paddle webhook, but can't get the verification to work. I tried various ways, here is one of them. I'll go through it on my console:
Here is the data:
data = {
"alert_id"=>"1",
"alert_name"=>"alert_created",
"cancel_url"=>"https://...",
"checkout_id"=>"1",
"user_id"=>"1",
"p_signature"=>"fwWXqR9C..."
}
public_key = "-----BEGIN PUBLIC KEY-----
FAKEdfsdfsdfsdfsdfsdfsdf23423423423423KCAgEAnyBxU0cUW9NGQU1Ur0MD
/M+VxdMsv7PMXsZFAKEKEYlskdfjlskdjflsjlskdj8i7+D9xZT57VhIfv8hInGy
IQFmJRrWxgmHjSW4cKbjg6Qg0NgOEuPdEIixsQZ/AfIar4KNObDul0EXL92B6ru/
...
-----END PUBLIC KEY-----"
Then I do the following:
signature = Base64.decode64(data['p_signature'])
data.delete('p_signature')
data.each {|key, value|data[key] = String(value)}
data_sorted = data.sort_by{|key, value| key}
data_serialized = data_sorted.to_json
digest = OpenSSL::Digest::SHA1.new
pub_key = OpenSSL::PKey::RSA.new(public_key)
verified = pub_key.verify(digest, signature, data_serialized)
In the end verified is always false. What am I doing wrong? I asked a related question here, but couldn't get it yet to work.
Example in paddle documentation that you've linked to suggests serializing data to PHP format before verification using php-serialize gem, that differs from regular json:
# ... data = ...
data_sorted.to_json
=> "[[\"alert_id\",\"1\"],[\"alert_name\",\"alert_created\"],[\"cancel_url\",\"https://...\"],[\"checkout_id\",\"1\"],[\"user_id\",\"1\"]]"
PHP.serialize(data_sorted, true)
=> "a:5:{s:8:\"alert_id\";s:1:\"1\";s:10:\"alert_name\";s:13:\"alert_created\";s:10:\"cancel_url\";s:11:\"https://...\";s:11:\"checkout_id\";s:1:\"1\";s:7:\"user_id\";s:1:\"1\";}"
as you can see - it gives different result, so obviously signature will not match.
Actually, you can take their whole ruby example:
require 'base64'
require 'php_serialize' # https://github.com/jqr/php-serialize
require 'openssl'
public_key = '-----BEGIN PUBLIC KEY-----
MIICIjANBgkqh...'
# 'data' represents all of the POST fields sent with the request.
signature = Base64.decode64(data['p_signature'])
data.delete('p_signature')
data.each {|key, value|data[key] = String(value)}
data_sorted = data.sort_by{|key, value| key}
data_serialized = PHP.serialize(data_sorted, true)
digest = OpenSSL::Digest::SHA1.new
pub_key = OpenSSL::PKey::RSA.new(public_key).public_key
verified = pub_key.verify(digest, signature, data_serialized)
I can't explain it completely but I think the false is related to your signature or data_serialized parameter, I use the same digest. Another difference is that sign is called on the key in the example below:
verify(digest, signature, data)
from: https://ruby-doc.org/stdlib-2.6.3/libdoc/openssl/rdoc/OpenSSL/PKey/PKey.html
To verify the String signature, digest, an instance of OpenSSL::Digest, must be provided to re-compute the message digest of the original data, also a String. The return value is true if the signature is valid, false otherwise. A PKeyError is raised should errors occur. Any previous state of the Digest instance is irrelevant to the validation outcome, the digest instance is reset to its initial state during the operation.
Example:
data = 'Sign me!'
digest = OpenSSL::Digest::SHA256.new
pkey = OpenSSL::PKey::RSA.new(2048)
signature = pkey.sign(digest, data)
pub_key = pkey.public_key
puts pub_key.verify(digest, signature, data) # => true`

Getting a bad decrypt error in ruby

I am using this snippet to extend the String class in Rails:
require 'openssl'
class String
def encrypt(key)
cipher = OpenSSL::Cipher.new('DES-EDE3-CBC').encrypt
key = cipher.random_key
cipher.key = key
s = cipher.update(self) + cipher.final
s.unpack('H*')[0].upcase
end
def decrypt(key)
cipher = OpenSSL::Cipher.new('DES-EDE3-CBC').decrypt
key = cipher.random_key
cipher.key = key
s = [self].pack("H*").unpack("C*").pack("c*")
cipher.update(s) + cipher.final
end
end
However when de-crypting the string I get the "Bad decrypt error":
puts plain = 'confidential' # confidential
puts key = 'secret' # secret
puts cipher = plain.encrypt(key) # 5C6D4C5FAFFCF09F271E01C5A132BE89
puts cipher.decrypt(key) # BAD DECRYPT
I tried adding padding like this to the decrypt action (Similar SO question here):
cipher.padding = 0
The error goes away but I am getting gibberish instead.
Even if you are passing key (secret) to the encrypt & decrypt functions, you are redefining key again with the below mentioned code.
key = cipher.random_key
You should be using same key for both encrypt & decrypt.
Try the below code snippet:
require 'openssl'
class String
def encrypt(key)
cipher = OpenSSL::Cipher.new('DES-EDE3-CBC').encrypt
cipher.key = (Digest::SHA1.hexdigest key)[0..23]
s = cipher.update(self) + cipher.final
s.unpack('H*')[0].upcase
end
def decrypt(key)
cipher = OpenSSL::Cipher.new('DES-EDE3-CBC').decrypt
cipher.key = (Digest::SHA1.hexdigest key)[0..23]
s = [self].pack("H*").unpack("C*").pack("c*")
cipher.update(s) + cipher.final
end
end
puts plain = 'confidential' # confidential
puts key = 'secret' # secret
puts cipher = plain.encrypt(key) # 5C6D4C5FAFFCF09F271E01C5A132BE89
puts cipher.decrypt(key) # confidential

How to decrypt a Rails 5 session cookie manually?

I have access to
config.action_dispatch.encrypted_cookie_salt
config.action_dispatch.encrypted_signed_cookie_salt
secrets.secret_key_base
the full cookie string (including --)
I see ways to do this in Rails 4 (Rails 4: How to decrypt rails 4 session cookie (Given the session key and secret)), but these don't seem to work in Rails 5.
I have had the same problem the other day and figured out that the generated secret was 64 bytes long (on my mac), but Rails ensures that the key is 32 bytes long (source).
This has worked for me:
require 'cgi'
require 'json'
require 'active_support'
def verify_and_decrypt_session_cookie(cookie, secret_key_base)
cookie = CGI::unescape(cookie)
salt = 'encrypted cookie'
signed_salt = 'signed encrypted cookie'
key_generator = ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)
secret = key_generator.generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len]
sign_secret = key_generator.generate_key(signed_salt)
encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON)
encryptor.decrypt_and_verify(cookie)
end
Or without ActiveSupport:
require 'openssl'
require 'base64'
require 'cgi'
require 'json'
def verify_and_decrypt_session_cookie(cookie, secret_key_base)
cookie = CGI.unescape(cookie)
#################
# generate keys #
#################
encrypted_cookie_salt = 'encrypted cookie' # default: Rails.application.config.action_dispatch.encrypted_cookie_salt
encrypted_signed_cookie_salt = 'signed encrypted cookie' # default: Rails.application.config.action_dispatch.encrypted_signed_cookie_salt
iterations = 1000
key_size = 64
secret = OpenSSL::PKCS5.pbkdf2_hmac_sha1(secret_key_base, encrypted_cookie_salt, iterations, key_size)[0, OpenSSL::Cipher.new('aes-256-cbc').key_len]
sign_secret = OpenSSL::PKCS5.pbkdf2_hmac_sha1(secret_key_base, encrypted_signed_cookie_salt, iterations, key_size)
##########
# Verify #
##########
data, digest = cookie.split('--')
raise 'invalid message' unless digest == OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, sign_secret, data)
# you better use secure compare instead of `==` to prevent time based attact,
# ref: ActiveSupport::SecurityUtils.secure_compare
###########
# Decrypt #
###########
encrypted_message = Base64.strict_decode64(data)
encrypted_data, iv = encrypted_message.split('--').map{|v| Base64.strict_decode64(v) }
cipher = OpenSSL::Cipher.new('aes-256-cbc')
cipher.decrypt
cipher.key = secret
cipher.iv = iv
decrypted_data = cipher.update(encrypted_data)
decrypted_data << cipher.final
JSON.load(decrypted_data)
end
Feel free to comment on the gist: https://gist.github.com/mbyczkowski/34fb691b4d7a100c32148705f244d028
Here's a Rails 5.2 variant of #matb's answer, which handles the revised configuration, encryption and serialization:
require 'cgi'
require 'active_support'
def verify_and_decrypt_session_cookie(cookie, secret_key_base = Rails.application.secret_key_base)
cookie = CGI::unescape(cookie)
salt = 'authenticated encrypted cookie'
encrypted_cookie_cipher = 'aes-256-gcm'
serializer = ActiveSupport::MessageEncryptor::NullSerializer
key_generator = ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)
key_len = ActiveSupport::MessageEncryptor.key_len(encrypted_cookie_cipher)
secret = key_generator.generate_key(salt, key_len)
encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: encrypted_cookie_cipher, serializer: serializer)
encryptor.decrypt_and_verify(cookie)
end
Also up at https://gist.github.com/inopinatus/e523f36b468f94cf6d34410b73fef15e.

how to use the bitbucket API with ruby and access token

I am trying to use the bitbucket API. I have successfully got the flow working where I am able to retrieve the access token and access token secret. After that, I have not been able to get anything to work. I can't find any example on to get this to work with Ruby. The closest I think I've found is this link:
https://gist.github.com/erikeldridge/383159
However in this example, he doesn't add the user's token and token secret, so I've updated it, here's his utility code:
# A utility for signing an url using OAuth in a way that's convenient for debugging
# Note: the standard Ruby OAuth lib is here http://github.com/mojodna/oauth
# License: http://gist.github.com/375593
# Usage: see example.rb below
require 'uri'
require 'cgi'
require 'openssl'
require 'base64'
class OauthUtil
attr_accessor :consumer_key, :consumer_secret, :token, :token_secret, :req_method,
:sig_method, :oauth_version, :callback_url, :params, :req_url, :base_str
def initialize
#consumer_key = ''
#consumer_secret = ''
#token = ''
#token_secret = ''
#req_method = 'GET'
#sig_method = 'HMAC-SHA1'
#oauth_version = '1.0'
#callback_url = ''
end
# openssl::random_bytes returns non-word chars, which need to be removed. using alt method to get length
# ref http://snippets.dzone.com/posts/show/491
def nonce
Array.new( 5 ) { rand(256) }.pack('C*').unpack('H*').first
end
def percent_encode( string )
# ref http://snippets.dzone.com/posts/show/1260
return URI.escape( string, Regexp.new("[^# {URI::PATTERN::UNRESERVED}]") ).gsub('*', '%2A')
end
# #ref http://oauth.net/core/1.0/#rfc.section.9.2
def signature
key = percent_encode( #consumer_secret ) + '&' + percent_encode( #token_secret )
# ref: http://blog.nathanielbibler.com/post/63031273/openssl-hmac-vs-ruby-hmac-benchmarks
digest = OpenSSL::Digest::Digest.new( 'sha1' )
hmac = OpenSSL::HMAC.digest( digest, key, #base_str )
# ref http://groups.google.com/group/oauth-ruby/browse_thread/thread/9110ed8c8f3cae81
Base64.encode64( hmac ).chomp.gsub( /\n/, '' )
end
# sort (very important as it affects the signature), concat, and percent encode
# #ref http://oauth.net/core/1.0/#rfc.section.9.1.1
# #ref http://oauth.net/core/1.0/#9.2.1
# #ref http://oauth.net/core/1.0/#rfc.section.A.5.1
def query_string
pairs = []
#params.sort.each { | key, val |
pairs.push( "#{ percent_encode( key ) }=#{ percent_encode( val.to_s ) }" )
}
pairs.join '&'
end
# organize params & create signature
def sign( parsed_url )
#params = {
'oauth_consumer_key' => #consumer_key,
'oauth_nonce' => nonce,
'oauth_signature_method' => #sig_method,
'oauth_timestamp' => Time.now.to_i.to_s,
'oauth_version' => #oauth_version
}
# if url has query, merge key/values into params obj overwriting defaults
if parsed_url.query
#params.merge! CGI.parse( parsed_url.query )
end
# #ref http://oauth.net/core/1.0/#rfc.section.9.1.2
#req_url = parsed_url.scheme + '://' + parsed_url.host + parsed_url.path
# create base str. make it an object attr for ez debugging
# ref http://oauth.net/core/1.0/#anchor14
#base_str = [
#req_method,
percent_encode( req_url ),
# normalization is just x-www-form-urlencoded
percent_encode( query_string )
].join( '&' )
# add signature
#params[ 'oauth_signature' ] = signature
return self
end
end
and here's my modified code:
require 'oauth_util.rb'
require 'net/http'
o = OauthUtil.new
o.consumer_key = MY_CONSUMER_KEY
o.consumer_secret = MY_SECRET
o.token = ACCESS_TOKEN_RETURNED_FROM_BB
o.token_secret = ACCESS_TOKEN_SECRET_RETURNED_FROM_BB
url = 'https://bitbucket.org/api/1.0/user'
parsed_url = URI.parse( url )
Net::HTTP.start( parsed_url.host ) { | http |
req = Net::HTTP::Get.new "#{ parsed_url.path }?#{ o.sign(parsed_url).query_string }"
response = http.request(req)
print response.read_body
}
Sadly, all I get is
301 Moved Permanently
301 Moved Permanently
nginx/1.5.10
Anyone have any luck using the BB API in Ruby after getting an access token back? Thanks for any help,
Kevin
EDIT:
There is a bitbucket API wrapper gem that has authentication built-in.
ORIGINAL:
I'm wondering if the "301 moved permanently" error is caused by the code making an http request instead of https.
without ssl:
result = Net::HTTP.get(URI.parse('http://bitbucket.org/api/1.0/user'))
# this returns 301 Moved Permanently
But when I used ssl instead (without the oauth headers/params) I got 401 Unauthorized.
with ssl:
1.9.3-p194 :063 > uri = URI.parse('https://bitbucket.org/api/1.0/user')
=> #<URI::HTTPS:0x007f846c5822d8 URL:https://bitbucket.org/api/1.0/user>
1.9.3-p194 :064 > http = Net::HTTP.new(uri.host, uri.port)
=> #<Net::HTTP bitbucket.org:443 open=false>
1.9.3-p194 :065 > http.use_ssl = true
=> true
1.9.3-p194 :066 > request = Net::HTTP::Get.new(uri.request_uri)
=> #<Net::HTTP::Get GET>
1.9.3-p194 :067 > response = http.request(request)
=> #<Net::HTTPUnauthorized 401 UNAUTHORIZED readbody=true>

Resources