mach 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
19
+ .rbenv-version
20
+ bin
21
+ gems
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format doc
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm --create ruby-1.9.3-p194@mach
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,4 @@
1
+ mach
2
+ ====
3
+
4
+ HMAC authentication stuff
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/examples/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source :rubygems
2
+
3
+ gem 'sinatra'
4
+ gem 'multi_json'
5
+ gem 'mach', :path => '../'
6
+
7
+ gem 'redis'
8
+ gem 'rack'
9
+
10
+ gem 'credential_store', :git => 'git@github.com:playup/credential_store.git'
11
+ gem 'rake'
@@ -0,0 +1,56 @@
1
+ GIT
2
+ remote: git@github.com:playup/credential_store.git
3
+ revision: 715e80adadc12bf5290dde096eaf3159a2d4a779
4
+ specs:
5
+ credential_store (0.0.5)
6
+ em-redis
7
+ em-synchrony
8
+ faraday
9
+ faraday_middleware
10
+ redis
11
+
12
+ PATH
13
+ remote: ../
14
+ specs:
15
+ mach (0.0.1)
16
+ faraday
17
+ multi_json
18
+ rack
19
+ redis
20
+
21
+ GEM
22
+ remote: http://rubygems.org/
23
+ specs:
24
+ em-redis (0.3.0)
25
+ eventmachine
26
+ em-synchrony (1.0.2)
27
+ eventmachine (>= 1.0.0.beta.1)
28
+ eventmachine (1.0.0.rc.4)
29
+ faraday (0.8.4)
30
+ multipart-post (~> 1.1)
31
+ faraday_middleware (0.8.8)
32
+ faraday (>= 0.7.4, < 0.9)
33
+ multi_json (1.3.6)
34
+ multipart-post (1.1.5)
35
+ rack (1.4.1)
36
+ rack-protection (1.2.0)
37
+ rack
38
+ rake (0.9.2.2)
39
+ redis (3.0.1)
40
+ sinatra (1.3.3)
41
+ rack (~> 1.3, >= 1.3.6)
42
+ rack-protection (~> 1.2)
43
+ tilt (~> 1.3, >= 1.3.3)
44
+ tilt (1.3.3)
45
+
46
+ PLATFORMS
47
+ ruby
48
+
49
+ DEPENDENCIES
50
+ credential_store!
51
+ mach!
52
+ multi_json
53
+ rack
54
+ rake
55
+ redis
56
+ sinatra
data/examples/Rakefile ADDED
@@ -0,0 +1,26 @@
1
+ require 'rack'
2
+ require 'rack/handler/webrick'
3
+
4
+ Dir['*.ru'].each do |server_config|
5
+ server_path = Pathname.new(server_config)
6
+ server_name = server_path.basename(server_path.extname)
7
+ namespace server_name.to_s.to_sym do
8
+ desc "Start the #{server_name}"
9
+ task :start do
10
+ Rack::Server.start(:config => server_config)
11
+ end
12
+ end
13
+ end
14
+
15
+ task :client do
16
+ puts `ruby ./client.rb`
17
+ end
18
+
19
+ task :help do
20
+ puts 'Start the credential server first like so: rake credential_server:start'
21
+ puts 'Then start the validating server like so: rake validating_server:start'
22
+ puts 'And run the clien like: rake client'
23
+ end
24
+
25
+ task :default => :help
26
+
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+ require 'bundler/setup'
3
+ require 'mach'
4
+ require 'base64'
5
+ require 'multi_json'
6
+
7
+ def get_credentials
8
+ connection = Faraday.new(:url => "http://localhost:9595") do |c|
9
+ c.adapter Faraday.default_adapter
10
+ end
11
+ credentials_response = connection.post { |req| req.url "/credentials" } # first request is to store the client delta
12
+ credentials = MultiJson.decode(credentials_response.body)
13
+ end
14
+
15
+ def make_request(id, secret)
16
+ #make a request using those credentials
17
+ connection = Faraday.new(:url => "http://localhost:9494") do |c|
18
+ c.request :hmac_authentication, id, secret
19
+ c.adapter Faraday.default_adapter
20
+ end
21
+ res = connection.get { |req| req.url '/' }
22
+ [res.status, res.body]
23
+ end
24
+
25
+ def make_valid_request
26
+ credentials = get_credentials
27
+ make_request(credentials["id"], credentials["secret"])
28
+ end
29
+
30
+ def make_invalid_request
31
+ credentials = get_credentials
32
+ make_request(credentials["id"], "XXX")
33
+ end
34
+
35
+ p make_valid_request
36
+ p make_invalid_request
@@ -0,0 +1,46 @@
1
+ #\ -p 9595
2
+
3
+ require 'bundler/setup'
4
+ require 'sinatra/base'
5
+ require 'mach'
6
+ require 'securerandom'
7
+ require 'openssl'
8
+ require 'base64'
9
+ require 'multi_json'
10
+
11
+ @@credential_store = {}
12
+
13
+ class App < Sinatra::Base
14
+ post '/credentials' do
15
+ content_type 'application/json'
16
+ credentials = CredentialsGenerator.generate_id_secret_pair
17
+ @@credential_store[credentials[:id]] = credentials[:secret]
18
+ MultiJson.encode(credentials)
19
+ end
20
+
21
+ get '/credentials/:id' do |id|
22
+ MultiJson.encode({:id => id, :secret => @@credential_store[id]})
23
+ end
24
+ end
25
+
26
+
27
+ class CredentialsGenerator
28
+ class << self
29
+ def generate_id
30
+ SecureRandom.base64(24).tr('+/=lIO0', 'pqrsxyz')
31
+ end
32
+
33
+ def generate_secret
34
+ Base64.strict_encode64(OpenSSL::Cipher::Cipher.new('aes-256-cbc').random_key)
35
+ end
36
+
37
+ def generate_id_secret_pair
38
+ id = generate_id
39
+ secret = generate_secret
40
+ {:id => id, :secret => secret}
41
+ end
42
+ end
43
+ end
44
+
45
+
46
+ run App.new
@@ -0,0 +1,27 @@
1
+ #\ -p 9494
2
+ require 'bundler/setup'
3
+ require 'sinatra/base'
4
+ require 'mach'
5
+ require 'base64'
6
+ require 'mach/rack/request_validator'
7
+ require 'credential_store'
8
+
9
+ #MAC_ID = "abc"
10
+ #MAC_KEY = Base64.strict_encode64("123")
11
+
12
+ Mach.configuration do |config|
13
+ config.with_credential_store CredentialStore::Adapter::CredentialServer.new(:server => 'http://localhost:9595/')
14
+ config.with_stale_request_window 10
15
+ config.with_data_store :redis, :host => "localhost", :port => "6379"
16
+ end
17
+
18
+ class App < Sinatra::Base
19
+ use Mach::Rack::RequestValidator
20
+
21
+ get '/' do
22
+ p (Mach::RequestValidator.valid?(request)) ? "Request is valid" : "Request is not valid"
23
+ 'OK'
24
+ end
25
+ end
26
+
27
+ run App.new
data/lib/mach.rb ADDED
@@ -0,0 +1,31 @@
1
+ require 'mach/faraday/request/hmac_authentication'
2
+ require 'mach/configuration'
3
+ require 'mach/validation/request_validator'
4
+
5
+ module Mach
6
+ if ::Faraday.respond_to? :register_middleware
7
+ ::Faraday.register_middleware :request, :hmac_authentication => lambda { Mach::Faraday::HmacAuthentication }
8
+ end
9
+ class << self
10
+ def configuration(&block)
11
+ @configuration ||= Mach::Configuration.new
12
+ if block_given?
13
+ block.call(@configuration)
14
+ else
15
+ @configuration
16
+ end
17
+ end
18
+ alias :config :configuration # can use either config or configuration
19
+
20
+ def respond_to?(method, include_private=false)
21
+ self.configuration.respond_to?(method, include_private) || super
22
+ end
23
+
24
+ private
25
+
26
+ def method_missing(method, *args, &block)
27
+ return super unless self.configuration.respond_to?(method)
28
+ self.configuration.send(method, *args, &block)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,46 @@
1
+ require 'mach/timestamp'
2
+ require 'mach/nonce'
3
+ require 'mach/normalized_string'
4
+ require 'mach/signature'
5
+
6
+ module Mach
7
+ class AuthorizationHeader
8
+ def initialize(id, key, options = {})
9
+ @id = id
10
+ @key = key
11
+ @options = options
12
+ end
13
+
14
+ def to_s
15
+ "MAC #{build_auth_value}"
16
+ end
17
+
18
+ private
19
+
20
+ def build_auth_value
21
+ timestamp = create_timestamp
22
+ nonce = create_nonce_for(timestamp)
23
+ normalized_string = NormalizedString.new(:timestamp => timestamp,
24
+ :nonce => nonce,
25
+ :request_method => @options[:request_method],
26
+ :path => @options[:path],
27
+ :host => @options[:host],
28
+ :port => @options[:port]
29
+ )
30
+ mac = Mach::Signature.new(@key, normalized_string.to_s) #sign_normalized_string(env, timestamp, nonce)
31
+ "id=\"#{@id}\",ts=\"#{timestamp}\",nonce=\"#{nonce_for_header(nonce)}\",mac=\"#{mac}\""
32
+ end
33
+
34
+ def create_timestamp
35
+ Mach::Timestamp.now
36
+ end
37
+
38
+ def create_nonce_for(timestamp)
39
+ Mach::Nonce.for(timestamp)
40
+ end
41
+
42
+ def nonce_for_header(actual_nonce)
43
+ actual_nonce
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,38 @@
1
+ require 'mach/persistence/in_memory_store'
2
+ require 'mach/persistence/redis_store'
3
+ require 'base64'
4
+
5
+ module Mach
6
+ class Configuration
7
+
8
+ attr_reader :credential_store, :data_store, :stale_request_window
9
+
10
+ def initialize
11
+ @stale_request_window = 10
12
+ @data_store = Mach::Persistence::InMemoryStore.configure({})
13
+ @credential_store = Hash.new
14
+ end
15
+
16
+ def with_credential_store(store, options = {})
17
+ @credential_store = store
18
+ #store_class = CredentialStore::Adapter.const_get(camelize(store_type.to_s))
19
+ #@credential_store = store_class.new(options)
20
+ # find out if we should use redis store first
21
+ #@credential_store = CredentialStore::Adapter::Redis.new() #todo pass options
22
+ end
23
+
24
+ def with_data_store(store_identifier, options = {})
25
+ store_class = Mach::Persistence.const_get(camelize("#{store_identifier.to_s}_store"))
26
+ @data_store = store_class.configure(options)
27
+ end
28
+
29
+ def with_stale_request_window(num_seconds)
30
+ @stale_request_window = num_seconds
31
+ end
32
+
33
+ private
34
+ def camelize(string)
35
+ string.split(/[^a-z0-9]/i).map{|w| w.capitalize}.join
36
+ end
37
+ end
38
+ end
data/lib/mach/delta.rb ADDED
@@ -0,0 +1,15 @@
1
+ module Mach
2
+ class Delta
3
+ class << self
4
+ def present_for(credential_id)
5
+ Mach.configuration.data_store.find_delta_by(credential_id)
6
+ end
7
+
8
+ def create(credential_id, server_timestamp, client_timestamp)
9
+ delta_value = server_timestamp - client_timestamp
10
+ expires_in = Mach.configuration.stale_request_window
11
+ Mach.configuration.data_store.add_delta(credential_id, delta_value, expires_in)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ module Mach
2
+ module Error
3
+ class RequestNotMacAuthenticatedError < StandardError
4
+ end
5
+ class CredentialFetchingError < StandardError
6
+ end
7
+ class MissingConfigurationOptionError < StandardError
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,63 @@
1
+ require 'faraday'
2
+ require 'mach/hmac'
3
+ require 'mach/authorization_header'
4
+
5
+ module Mach
6
+ module Faraday
7
+ class HmacAuthentication < ::Faraday::Middleware
8
+ include Mach::HMAC
9
+ KEY = "Authorization".freeze
10
+
11
+ def initialize(app, id, key)
12
+ @mac_id = id
13
+ @mac_key = key
14
+ super(app)
15
+ end
16
+
17
+ def call(env)
18
+ unless env[:request_headers][KEY]
19
+ env[:request_headers][KEY] = self.header(env)
20
+ end
21
+ @app.call(env)
22
+ end
23
+
24
+ def header(env)
25
+ AuthorizationHeader.new(@mac_id, @mac_key,
26
+ :request_method => mac_request_method(request_method(env)),
27
+ :path => mac_path(path(env), query_string(env)),
28
+ :host => mac_host(host(env)),
29
+ :port => mac_port(port(env), scheme(env)),
30
+ :ext => mac_ext(ext(env))).to_s
31
+ end
32
+
33
+ private
34
+ def request_method(env)
35
+ env[:method]
36
+ end
37
+
38
+ def host(env)
39
+ env[:url].host
40
+ end
41
+
42
+ def port(env)
43
+ env[:url].port
44
+ end
45
+
46
+ def path(env)
47
+ env[:url].path
48
+ end
49
+
50
+ def query_string(env)
51
+ env[:url].query
52
+ end
53
+
54
+ def ext(env)
55
+ nil
56
+ end
57
+
58
+ def scheme(env)
59
+ env[:url].scheme
60
+ end
61
+ end
62
+ end
63
+ end
data/lib/mach/hmac.rb ADDED
@@ -0,0 +1,27 @@
1
+ module Mach
2
+ module HMAC
3
+ def mac_request_method(request_method)
4
+ request_method.to_s.upcase
5
+ end
6
+
7
+ def mac_path(path, query_string)
8
+ query_string && !query_string.empty? ? "#{path}?#{query_string}" : "#{path}"
9
+ end
10
+
11
+ def mac_host(host)
12
+ host
13
+ end
14
+
15
+ def mac_port(port, scheme)
16
+ unless port
17
+ port = 80 if scheme == 'http'
18
+ port = 443 if scheme == 'https'
19
+ end
20
+ port
21
+ end
22
+
23
+ def mac_ext(ext)
24
+ ext
25
+ end
26
+ end
27
+ end
data/lib/mach/nonce.rb ADDED
@@ -0,0 +1,22 @@
1
+ require 'securerandom'
2
+ require 'base64'
3
+
4
+ module Mach
5
+ class Nonce
6
+ class << self
7
+ def for(timestamp)
8
+ #17 byte string to make sure we don't get any padding after base64
9
+ Base64.urlsafe_encode64("#{SecureRandom.random_bytes(17)}#{timestamp}")
10
+ end
11
+
12
+ def exists?(credential_id, nonce_value)
13
+ Mach.configuration.data_store.find_nonce_by(credential_id, nonce_value)
14
+ end
15
+
16
+ def persist(credential_id, nonce_value, timestamp)
17
+ expires_in = Mach.configuration.stale_request_window
18
+ Mach.configuration.data_store.add_nonce(credential_id, nonce_value, expires_in)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ module Mach
2
+ class NormalizedString
3
+ def initialize(options = {})
4
+ @timestamp = options[:timestamp]
5
+ @nonce = options[:nonce]
6
+ @request_method = options[:request_method]
7
+ @path = options[:path]
8
+ @host = options[:host]
9
+ @port = options[:port]
10
+ @ext = options[:ext] || "\n"
11
+ end
12
+
13
+ def to_s
14
+ [@timestamp, @nonce, @request_method, @path, @host, @port, @ext].join("\n")
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,27 @@
1
+ module Mach
2
+ module Persistence
3
+ class DeltaAndNonceStore
4
+ class << self
5
+ def configure(options = {})
6
+ self.new(options)
7
+ end
8
+ end
9
+
10
+ def find_delta_by(credential_id)
11
+ raise "Implement me"
12
+ end
13
+
14
+ def add_delta(credential_id, delta_value, expires_in)
15
+ raise "Implement me"
16
+ end
17
+
18
+ def find_nonce_by(credential_id, nonce_value)
19
+ raise "Implement me"
20
+ end
21
+
22
+ def add_nonce(credential_id, nonce_value, timestamp)
23
+ raise "Implement me"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,48 @@
1
+ require 'mach/persistence/delta_and_nonce_store'
2
+ require 'mach/timestamp'
3
+
4
+ module Mach
5
+ module Persistence
6
+ class InMemoryStore < Mach::Persistence::DeltaAndNonceStore
7
+ def initialize(options = {})
8
+ @deltas = {}
9
+ @nonces = {}
10
+ end
11
+
12
+ def find_delta_by(credential_id)
13
+ delta_hash = @deltas[credential_id] || {}
14
+ now = Timestamp.now
15
+ if delta_hash[:expires_at] && delta_hash[:expires_at] > now
16
+ delta_hash[:delta_value]
17
+ else
18
+ @deltas[credential_id] = nil
19
+ nil
20
+ end
21
+ end
22
+
23
+ def add_delta(credential_id, delta_value, expires_in)
24
+ expires_at = Timestamp.now + expires_in
25
+ @deltas[credential_id] = {:delta_value => delta_value, :expires_at => expires_at}
26
+ @deltas[credential_id][:delta_value]
27
+ end
28
+
29
+ def find_nonce_by(credential_id, nonce_value)
30
+ nonces_for_credential_id = @nonces[credential_id] || {}
31
+ expires_at = nonces_for_credential_id[nonce_value]
32
+ now = Timestamp.now
33
+ if expires_at && expires_at > now
34
+ nonce_value
35
+ else
36
+ nonces_for_credential_id[nonce_value] = nil
37
+ nil
38
+ end
39
+ end
40
+
41
+ def add_nonce(credential_id, nonce_value, expires_in)
42
+ expires_at = Timestamp.now + expires_in
43
+ @nonces[credential_id] ||= {}
44
+ @nonces[credential_id][nonce_value] = expires_at
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,40 @@
1
+ require 'mach/persistence/delta_and_nonce_store'
2
+ require 'mach/timestamp'
3
+ require 'redis'
4
+
5
+ module Mach
6
+ module Persistence
7
+ class RedisStore < Mach::Persistence::DeltaAndNonceStore
8
+ def initialize(options = {})
9
+ raise MissingConfigurationOptionError unless options[:host] && options[:port]
10
+ @redis = Redis.new(:host => options[:host], :port => options[:port])
11
+ end
12
+
13
+ def find_delta_by(credential_id)
14
+ @redis.get(delta_key_for(credential_id))
15
+ end
16
+
17
+ def add_delta(credential_id, delta_value, expires_in)
18
+ @redis.setex(delta_key_for(credential_id), expires_in, delta_value)
19
+ delta_value
20
+ end
21
+
22
+ def find_nonce_by(credential_id, nonce_value)
23
+ @redis.get(nonce_key_for(credential_id, nonce_value))
24
+ end
25
+
26
+ def add_nonce(credential_id, nonce_value, expires_in)
27
+ @redis.setex(nonce_key_for(credential_id, nonce_value), expires_in, 1)
28
+ end
29
+
30
+ private
31
+ def delta_key_for(credential_id)
32
+ "delta_key_for_#{credential_id}"
33
+ end
34
+
35
+ def nonce_key_for(credential_id, nonce)
36
+ "nonce_key_for_#{credential_id}_#{nonce}"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,29 @@
1
+ module Mach
2
+ module Rack
3
+ class RequestValidator
4
+ def initialize app
5
+ @app = app
6
+ end
7
+
8
+ def call env
9
+ request = Mach::Request.new(env)
10
+ if request.mac_authorization? && !Mach::RequestValidator.valid?(request)
11
+ failure
12
+ else
13
+ success(env)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def success(env)
20
+ @app.call(env)
21
+ end
22
+
23
+ def failure
24
+ [401, { 'Content-Type' => 'application/json' }, ['Unauthorized'] ]
25
+ end
26
+ end
27
+ end
28
+ end
29
+
@@ -0,0 +1,51 @@
1
+ require 'rack'
2
+ require 'mach/normalized_string'
3
+
4
+ module Mach
5
+ class Request < ::Rack::Request
6
+ include Mach::HMAC
7
+
8
+ def authorization
9
+ @env['HTTP_AUTHORIZATION']
10
+ end
11
+
12
+ def mac_authorization?
13
+ !!(self.authorization =~ /^\s*MAC .*$/)
14
+ end
15
+
16
+ def mac_id
17
+ self.authorization.scan(/^\s*MAC\s*.*id="(.*?)".*/).flatten[0]
18
+ end
19
+
20
+ def mac_timestamp
21
+ self.authorization.scan(/^\s*MAC\s*.*ts="(.*?)".*/).flatten[0]
22
+ end
23
+
24
+ def mac_nonce
25
+ self.authorization.scan(/^\s*MAC\s*.*nonce="(.*?)".*/).flatten[0]
26
+ end
27
+
28
+ def mac_ext
29
+ self.authorization.scan(/^\s*MAC\s*.*ext="(.*?)".*/).flatten[0]
30
+ end
31
+
32
+ def mac_signature
33
+ self.authorization.scan(/^\s*MAC\s*.*mac="(.*?)".*/).flatten[0]
34
+ end
35
+
36
+ def mac_normalized_request_string
37
+ if mac_authorization?
38
+ NormalizedString.new(:timestamp => mac_timestamp,
39
+ :nonce => mac_nonce,
40
+ :request_method => request_method,
41
+ :path => mac_path(path, query_string),
42
+ :host => host,
43
+ :port => mac_port(self.port, self.scheme),
44
+ :ext => mac_ext
45
+ ).to_s
46
+ else
47
+ ""
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,30 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ module Mach
5
+ class Signature
6
+ ALGORITHMS = {'hmac-sha-256' => 'sha256', 'hmac-sha-1' => 'sha1'}
7
+ DEFAULT_ALGORITHM = 'hmac-sha-256'
8
+
9
+ def initialize(key, data, algorithm = DEFAULT_ALGORITHM)
10
+ @algorithm = algorithm
11
+ @data = data
12
+ @key = key
13
+ end
14
+
15
+ def to_s
16
+ Base64.strict_encode64(OpenSSL::HMAC.digest(digest, @key, @data))
17
+ end
18
+
19
+ def matches?(base64_expected_signature)
20
+ to_s == base64_expected_signature
21
+ end
22
+
23
+ private
24
+
25
+ def digest
26
+ digest_algorithm = @algorithm ? ALGORITHMS[@algorithm] : ALGORITHMS[DEFAULT_ALGORITHM]
27
+ OpenSSL::Digest::Digest.new(digest_algorithm)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,9 @@
1
+ module Mach
2
+ class Timestamp
3
+ class << self
4
+ def now
5
+ Time.now.utc.to_i
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ module Mach
2
+ module Validation
3
+ class NonceValidator
4
+ class << self
5
+ def valid?(hmac_request)
6
+ !Nonce.exists?(hmac_request.mac_id, hmac_request.mac_nonce)
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ require 'mach/error/error'
2
+ require 'mach/request'
3
+ require 'mach/validation/timestamp_validator'
4
+ require 'mach/validation/nonce_validator'
5
+ require 'mach/validation/signature_validator'
6
+
7
+ module Mach
8
+ class RequestValidator
9
+ class << self
10
+ def valid?(rack_request)
11
+ hmac_request = Mach::Request.new(rack_request.env)
12
+ raise Mach::Error::RequestNotMacAuthenticatedError unless hmac_request.mac_authorization?
13
+ valid = hmac_request.mac_id &&
14
+ Mach::Validation::TimestampValidator.valid?(hmac_request) &&
15
+ Mach::Validation::NonceValidator.valid?(hmac_request) &&
16
+ Mach::Validation::SignatureValidator.valid?(hmac_request)
17
+ #need to make sure we store the nonce
18
+ Nonce.persist(hmac_request.mac_id, hmac_request.mac_nonce, hmac_request.mac_timestamp.to_i) if valid
19
+ valid
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ require 'mach/signature'
2
+
3
+ module Mach
4
+ module Validation
5
+
6
+ class SignatureValidator
7
+ class << self
8
+ def valid?(hmac_request)
9
+ if secret = credential_store[hmac_request.mac_id]
10
+ data = hmac_request.mac_normalized_request_string
11
+ Mach::Signature.new(secret, data).matches?(hmac_request.mac_signature)
12
+ else
13
+ false
14
+ end
15
+ end
16
+
17
+ private
18
+ def credential_store
19
+ Mach::configuration.credential_store
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,21 @@
1
+ require 'mach/timestamp'
2
+ require 'mach/delta'
3
+
4
+ module Mach
5
+ module Validation
6
+ class TimestampValidator
7
+ class << self
8
+ def valid?(hmac_request)
9
+ server_timestamp = Mach::Timestamp.now.to_i
10
+ client_timestamp = hmac_request.mac_timestamp.to_i
11
+ delta = Mach::Delta.present_for(hmac_request.mac_id) #do we have a delta for this client
12
+ unless delta
13
+ delta = Mach::Delta.create(hmac_request.mac_id, server_timestamp, client_timestamp)
14
+ end
15
+ #make sure the client timestamp is not older than what we're willing to accept
16
+ server_timestamp - (client_timestamp + delta.to_i) < Mach.configuration.stale_request_window
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module Mach
2
+ VERSION = "0.0.1"
3
+ end
data/mach.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "mach/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "mach"
7
+ s.version = Mach::VERSION
8
+ s.authors = ["Alan Skorkin"]
9
+ s.email = ["alan@skorks.com"]
10
+ s.homepage = "https://github.com/skorks/mach"
11
+ s.summary = %q{HMAC authentication stuff}
12
+ s.description = %q{HMAC authentication stuff}
13
+
14
+ s.rubyforge_project = "mach"
15
+
16
+ s.add_dependency 'faraday'
17
+ s.add_dependency 'rack'
18
+ s.add_dependency 'multi_json'
19
+ s.add_dependency 'redis'
20
+
21
+ s.add_development_dependency 'rake'
22
+ s.add_development_dependency 'rspec'
23
+ s.add_development_dependency 'json'
24
+
25
+ s.files = `git ls-files`.split("\n")
26
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
27
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
28
+ s.require_paths = ["lib"]
29
+ end
@@ -0,0 +1,34 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+ require 'mach/normalized_string'
3
+
4
+ describe Mach::NormalizedString do
5
+ let(:timestamp) {1344824119}
6
+ let(:nonce) {"abc123"}
7
+ let(:request_method) {"GET"}
8
+ let(:path) {"/blah?yadda=5&foo=bar"}
9
+ let(:host) {"blah.com"}
10
+ let(:port) {80}
11
+ let(:ext) {"hello"}
12
+
13
+ let(:normalized_string) do
14
+ Mach::NormalizedString.new(:timestamp => timestamp,
15
+ :nonce => nonce,
16
+ :request_method => request_method,
17
+ :path => path,
18
+ :host => host,
19
+ :port => port,
20
+ :ext => ext
21
+ )
22
+ end
23
+
24
+ describe "#to_s" do
25
+ subject { normalized_string.to_s }
26
+ context "when all compenents present" do
27
+ it { subject.should == "#{timestamp}\n#{nonce}\n#{request_method}\n#{path}\n#{host}\n#{port}\n#{ext}" }
28
+ end
29
+ context "when no ext" do
30
+ let(:ext) {nil}
31
+ it { subject.should == "#{timestamp}\n#{nonce}\n#{request_method}\n#{path}\n#{host}\n#{port}\n\n" }
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,55 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+ require "base64"
3
+ require 'mach/signature'
4
+
5
+ describe Mach::Signature do
6
+ let(:key) { "some_key" }
7
+ let(:data) {"hello world"}
8
+
9
+ describe "#to_s" do
10
+ subject { Mach::Signature.new(key, data).to_s }
11
+
12
+ context "produces a signature" do
13
+ it { ->{subject}.should_not raise_error }
14
+ end
15
+ #this was to test ios integration and it seemed to work
16
+ #context "testing123" do
17
+ #let(:timestamp) {1344843814}
18
+ #let(:nonce) {"PYVV2cPkVvVsB0Gi"}
19
+ #let(:request_method) {"GET"}
20
+ #let(:path) {"/contests/contest-20120191956?subject_path=%2Fcontests%2F20120191956"}
21
+ #let(:host) {"staging.api.playupdev.com"}
22
+ #let(:port) {80}
23
+
24
+ #let(:data) do
25
+ #Mach::NormalizedString.new(:timestamp => timestamp,
26
+ #:nonce => nonce,
27
+ #:request_method => request_method,
28
+ #:path => path,
29
+ #:host => host,
30
+ #:port => port
31
+ #).to_s
32
+ #end
33
+
34
+ #let(:key) { "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }
35
+ #it {subject.should == "YnHx40PpfjMaxtO+sxJvg2XtOF70L1zGlNad92dg8i4="}
36
+ #end
37
+ end
38
+
39
+ describe "#matches?" do
40
+ let(:signature) { Mach::Signature.new(key, data) }
41
+ subject { signature.matches?(expected_signature) }
42
+
43
+
44
+ context "when expected signature is invalid" do
45
+ let(:expected_signature) { "abc123" }
46
+ it {subject.should_not be_true}
47
+ end
48
+ context "when expected signature is valid" do
49
+ let(:expected_signature) { '/wfbnU08rHnneh2Q4wSopDyULH43ePyuSyHMBc9nbnw=' }
50
+
51
+ it {subject.should be_true}
52
+ end
53
+ end
54
+ end
55
+
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+ $:.unshift(File.expand_path('../', __FILE__))
3
+ $:.unshift(File.expand_path('../../lib', __FILE__))
4
+
5
+ require 'mach'
6
+
7
+ # Requires supporting ruby files with custom matchers and macros, etc,
8
+ # in spec/support/ and its subdirectories.
9
+ Dir[File.join(File.expand_path('../support', __FILE__), '**/*.rb')].each {|f| require f}
10
+
11
+ # Use Mocha to mock with RSpec
12
+ RSpec.configure do |config|
13
+ config.mock_with :rspec
14
+ end
metadata ADDED
@@ -0,0 +1,200 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mach
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alan Skorkin
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-30 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: faraday
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rack
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: multi_json
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: redis
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rake
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: rspec
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: json
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: HMAC authentication stuff
127
+ email:
128
+ - alan@skorks.com
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - .gitignore
134
+ - .rspec
135
+ - .rvmrc
136
+ - Gemfile
137
+ - README.md
138
+ - Rakefile
139
+ - examples/Gemfile
140
+ - examples/Gemfile.lock
141
+ - examples/Rakefile
142
+ - examples/client.rb
143
+ - examples/credential_server.ru
144
+ - examples/validating_server.ru
145
+ - lib/mach.rb
146
+ - lib/mach/authorization_header.rb
147
+ - lib/mach/configuration.rb
148
+ - lib/mach/delta.rb
149
+ - lib/mach/error/error.rb
150
+ - lib/mach/faraday/request/hmac_authentication.rb
151
+ - lib/mach/hmac.rb
152
+ - lib/mach/nonce.rb
153
+ - lib/mach/normalized_string.rb
154
+ - lib/mach/persistence/delta_and_nonce_store.rb
155
+ - lib/mach/persistence/in_memory_store.rb
156
+ - lib/mach/persistence/redis_store.rb
157
+ - lib/mach/rack/request_validator.rb
158
+ - lib/mach/request.rb
159
+ - lib/mach/signature.rb
160
+ - lib/mach/timestamp.rb
161
+ - lib/mach/validation/nonce_validator.rb
162
+ - lib/mach/validation/request_validator.rb
163
+ - lib/mach/validation/signature_validator.rb
164
+ - lib/mach/validation/timestamp_validator.rb
165
+ - lib/mach/version.rb
166
+ - mach.gemspec
167
+ - spec/normalized_string_spec.rb
168
+ - spec/signature_spec.rb
169
+ - spec/spec_helper.rb
170
+ homepage: https://github.com/skorks/mach
171
+ licenses: []
172
+ post_install_message:
173
+ rdoc_options: []
174
+ require_paths:
175
+ - lib
176
+ required_ruby_version: !ruby/object:Gem::Requirement
177
+ none: false
178
+ requirements:
179
+ - - ! '>='
180
+ - !ruby/object:Gem::Version
181
+ version: '0'
182
+ segments:
183
+ - 0
184
+ hash: 1737604513296395942
185
+ required_rubygems_version: !ruby/object:Gem::Requirement
186
+ none: false
187
+ requirements:
188
+ - - ! '>='
189
+ - !ruby/object:Gem::Version
190
+ version: '0'
191
+ segments:
192
+ - 0
193
+ hash: 1737604513296395942
194
+ requirements: []
195
+ rubyforge_project: mach
196
+ rubygems_version: 1.8.24
197
+ signing_key:
198
+ specification_version: 3
199
+ summary: HMAC authentication stuff
200
+ test_files: []