Manually encrypting and decrypting cookie - Rails - ruby-on-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

Related

FTX.com REST API POST Authentication FAILS with Ruby on Rails and net/https

Hoping for some help as this one has me baffled...
I created a user account and API credentials at FTX.com.
They have an interesting Auth setup which is detailed here: https://docs.ftx.com/?python#authentication
They only provide code examples for python, javascript and c#, but I need to implement the integration on a RoR app.
Here's a link which also provides an example for both GET and POST calls: https://blog.ftx.com/blog/api-authentication/
I'm using:
ruby '3.0.1'
gem 'rails', '~> 6.1.4', '>= 6.1.4.1'
also,
require 'uri'
require 'net/https'
require 'net/http'
require 'json'
I got the authentication working for GET calls as follows:
def get_market
get_market_url = 'https://ftx.com/api/markets/BTC-PERP/orderbook?depth=20'
api_get_call(get_market_url)
end
def api_get_call(url)
ts = (Time.now.to_f * 1000).to_i
signature_payload = "#{ts}GET/api/markets"
key = ENV['FTX_API_SECRET']
data = signature_payload
digest = OpenSSL::Digest.new('sha256')
signature = OpenSSL::HMAC.hexdigest(digest, key, data)
headers = {
'FTX-KEY': ENV['FTX_API_KEY'],
'FTX-SIGN': signature,
'FTX-TS': ts.to_s
}
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)
http.read_timeout = 1200
http.use_ssl = true
rsp = http.get(uri, headers)
JSON.parse(rsp.body)
end
This works great and I get the correct response:
=>
{"success"=>true,
"result"=>
{"bids"=>
[[64326.0, 2.0309],
...
[64303.0, 3.1067]],
"asks"=>
[[64327.0, 4.647],
...
[64352.0, 0.01]]}}
However, I can't seem to authenticate correctly for POST calls (even though as far as I can tell I am following the instructions correctly). I use the following:
def create_subaccount
create_subaccount_url = 'https://ftx.com/api/subaccounts'
call_body =
{
"nickname": "sub2",
}.to_json
api_post_call(create_subaccount_url, call_body)
end
def api_post_call(url, body)
ts = (Time.now.to_f * 1000).to_i
signature_payload = "#{ts}POST/api/subaccounts#{body}"
key = ENV['FTX_API_SECRET']
data = signature_payload
digest = OpenSSL::Digest.new('sha256')
signature = OpenSSL::HMAC.hexdigest(digest, key, data)
headers = {
'FTX-KEY': ENV['FTX_API_KEY'],
'FTX-SIGN': signature,
'FTX-TS': ts.to_s
}
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)
http.read_timeout = 1200
http.use_ssl = true
request = Net::HTTP::Post.new(uri, headers)
request.body = body
response = http.request(request)
JSON.parse(response.body)
end
Also tried passing headers via request[] directly:
def api_post_call(url, body)
ts = (Time.now.to_f * 1000).to_i
signature_payload = "#{ts}POST/api/subaccounts#{body}"
key = ENV['FTX_API_SECRET']
data = signature_payload
digest = OpenSSL::Digest.new('sha256')
signature = OpenSSL::HMAC.hexdigest(digest, key, data)
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)
http.read_timeout = 1200
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['FTX-KEY'] = ENV['FTX_API_KEY']
request['FTX-SIGN'] = signature
request['FTX-TS'] = ts.to_s
request.body = body
response = http.request(request)
JSON.parse(response.body)
end
This is the error response:
=> {"success"=>false, "error"=>"Not logged in: Invalid signature"}
My feeling is the issue is somewhere in adding the body to signature_payload before generating the signature via HMAC here..?:
signature_payload = "#{ts}POST/api/subaccounts#{body}"
Thinking this because, if I leave out #{body} here, like so:
signature_payload = "#{ts}POST/api/subaccounts"
the response is:
=> {"success"=>false, "error"=>"Missing parameter nickname"}
I have tried several iterations of setting up the POST call method using various different net/https examples but have had no luck...
I have also contacted FTX support but have had no response.
Would truly appreciate if anyone has some insight on what I am doing wrong here?
try this headers
headers = {
'FTX-KEY': ENV['FTX_API_KEY'],
'FTX-SIGN': signature,
'FTX-TS': ts.to_s,
'Content-Type' => 'application/json',
'Accepts' => 'application/json',
}
Here's a working example of a class to retrieve FTX subaccounts. Modify for your own purposes. I use HTTParty.
class Balancer
require 'uri'
require "openssl"
include HTTParty
def get_ftx_subaccounts
method = 'GET'
path = '/subaccounts'
url = "#{ENV['FTX_BASE_URL']}#{path}"
return HTTParty.get(url, headers: headers(method, path, ''))
end
def headers(*args)
{
'FTX-KEY' => ENV['FTX_API_KEY'],
'FTX-SIGN' => signature(*args),
'FTX-TS' => ts.to_s,
'Content-Type' => 'application/json',
'Accepts' => 'application/json',
}
end
def signature(*args)
OpenSSL::HMAC.hexdigest(digest, ENV['FTX_API_SECRET'], signature_payload(*args))
end
def signature_payload(method, path, query)
payload = [ts, method.to_s.upcase, "/api", path].compact
if method==:post
payload << query.to_json
elsif method==:get
payload << ("?" + URI.encode_www_form(query))
end unless query.empty?
payload.join.encode("UTF-8")
end
def ts
#ts ||= (Time.now.to_f * 1000).to_i
end
def digest
#digest ||= OpenSSL::Digest.new('sha256')
end
end

AWS signature version 4 signing key issue ruby

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.

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

ruby-jwt encode, decode with RS256 algorithm

It seems like we have always initialize and use the same private key when encoding and decoding a token in RSA256 algorithm:
payload = {:data => 'test'}
rsa_private = OpenSSL::PKey::RSA.generate 2048
rsa_public = rsa_private.public_key
token = JWT.encode payload, rsa_private, 'RS256'
# eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ0ZXN0IjoiZGF0YSJ9.c2FynXNyi6_PeKxrDGxfS3OLwQ8lTDbWBWdq7oMviCy2ZfFpzvW2E_odCWJrbLof-eplHCsKzW7MGAntHMALXgclm_Cs9i2Exi6BZHzpr9suYkrhIjwqV1tCgMBCQpdeMwIq6SyKVjgH3L51ivIt0-GDDPDH1Rcut3jRQzp3Q35bg3tcI2iVg7t3Msvl9QrxXAdYNFiS5KXH22aJZ8X_O2HgqVYBXfSB1ygTYUmKTIIyLbntPQ7R22rFko1knGWOgQCoYXwbtpuKRZVFrxX958L2gUWgb4jEQNf3fhOtkBm1mJpj-7BGst00o8g_3P2zHy-3aKgpPo1XlKQGjRrrxA
puts token
decoded_token = JWT.decode token, rsa_public, true, { :algorithm => 'RS256' }
# Array
# [
# {"data"=>"test"}, # payload
# {"alg"=>"RS256"} # header
# ]
puts decoded_token
But what is the best way t do that in a Rails 5.1 app ?
I figured out how to fix that.
So I have a class JsonWebTokenwith 2 class methods: encodeand decodedefined as follows:
class JsonWebToken
ALGO = 'RS256'
class << self
def encode(payload, exp = 2.hours.from_now)
# set expiry to 2 hours from creation time
payload[:exp] = exp.to_i
JWT.encode(payload, private_key, ALGO)
end
def decode(token)
body = JWT.decode(token, private_key.public_key, true, algorithm: ALGO)[0]
HashWithIndifferentAccess.new body
# rescue from expiry exception
rescue JWT::ExpiredSignature, JWT::VerificationError => e
# raise custom error to be handled by custom handler
raise ExceptionHandler::ExpiredSignature, e.message
end
private
def private_key
#rsa_private ||= OpenSSL::PKey::RSA.generate 2048
end
end
end
I just use another private static method to generate an rsa private key if needed.

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.

Resources