signed_form 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ require 'signed_form/digest_stores/null_store'
2
+
3
+ module SignedForm
4
+ module DigestStores
5
+ autoload :MemoryStore, 'signed_form/digest_stores/memory_store'
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'active_support'
2
+
3
+ module SignedForm
4
+ module DigestStores
5
+ class MemoryStore < ActiveSupport::Cache::MemoryStore; end
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ module SignedForm
2
+ module DigestStores
3
+ class NullStore
4
+ def fetch(key)
5
+ yield
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,67 @@
1
+ require 'set'
2
+
3
+ module SignedForm
4
+ class Digestor
5
+ attr_accessor :view_paths
6
+
7
+ def initialize(template)
8
+ @view_paths = Set.new
9
+ @views = Set.new
10
+ self << template
11
+ end
12
+
13
+ def <<(template)
14
+ virtual_path = get_virtual_path(template)
15
+ raise Errors::UnableToDigest, "Unable to get virtual path from template" unless virtual_path
16
+
17
+ @views << virtual_path
18
+ @view_paths += template.view_paths.map(&:to_s)
19
+ @digest = nil
20
+ rescue NoMethodError
21
+ raise Errors::UnableToDigest, "Unable get view paths from template"
22
+ end
23
+
24
+ def marshal_dump
25
+ [@views.to_a, to_s]
26
+ end
27
+
28
+ def marshal_load(input)
29
+ @views, @digest = input
30
+ @view_paths = []
31
+ @digest.taint
32
+ end
33
+
34
+ def to_s
35
+ @digest ||= SignedForm.digest_store.fetch(@views.sort.join(':')) { hash_files(glob_files) }
36
+ end
37
+ alias_method :digest, :to_s
38
+
39
+ def refresh
40
+ @digest = nil
41
+ end
42
+
43
+ private
44
+
45
+ def glob_files
46
+ globbed_files = []
47
+ view_paths.each do |path|
48
+ @views.each { |view| globbed_files += Dir["#{path}/#{view}.*"] }
49
+ end
50
+ globbed_files
51
+ end
52
+
53
+ def hash_files(files)
54
+ raise Errors::UnableToDigest, "No files to digest" if files.empty?
55
+
56
+ md5 = Digest::MD5.new
57
+ files.sort.each do |entry|
58
+ File.open(entry) { |f| md5 << f.read }
59
+ end
60
+ md5.to_s
61
+ end
62
+
63
+ def get_virtual_path(template)
64
+ template.respond_to?(:virtual_path) ? template.virtual_path : template.instance_variable_get(:@virtual_path)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,5 @@
1
+ module SignedForm
2
+ module Rails
3
+ class Engine < ::Rails::Engine; end
4
+ end
5
+ end
@@ -1,7 +1,9 @@
1
1
  module SignedForm
2
2
  module Errors
3
- class NoSecretKey < StandardError; end
3
+ class NoSecretKey < StandardError; end
4
4
  class InvalidSignature < StandardError; end
5
- class InvalidURL < StandardError; end
5
+ class InvalidURL < StandardError; end
6
+ class UnableToDigest < StandardError; end
7
+ class ExpiredForm < StandardError; end
6
8
  end
7
9
  end
@@ -1,85 +1,99 @@
1
1
  module SignedForm
2
- class FormBuilder < ::ActionView::Helpers::FormBuilder
2
+ module FormBuilder
3
+ FIELDS_TO_SIGN = [:select, :collection_select, :grouped_collection_select,
4
+ :time_zone_select, :collection_radio_buttons, :collection_check_boxes,
5
+ :date_select, :datetime_select, :time_select,
6
+ :text_field, :password_field, :hidden_field,
7
+ :file_field, :text_area, :check_box,
8
+ :radio_button, :color_field,
9
+ :telephone_field, :phone_field, :date_field,
10
+ :time_field, :datetime_field, :datetime_local_field,
11
+ :month_field, :week_field, :url_field,
12
+ :email_field, :number_field, :range_field]
3
13
 
4
- # Base methods for form signing. Include this module in your own builders to get signatures for the base input
5
- # helpers. Add fields to sign with #add_signed_fields
6
- module Methods
7
- FIELDS_TO_SIGN = [:select, :collection_select, :grouped_collection_select,
8
- :time_zone_select, :collection_radio_buttons, :collection_check_boxes,
9
- :date_select, :datetime_select, :time_select,
10
- :text_field, :password_field, :hidden_field,
11
- :file_field, :text_area, :check_box,
12
- :radio_button, :color_field, :search_field,
13
- :telephone_field, :phone_field, :date_field,
14
- :time_field, :datetime_field, :datetime_local_field,
15
- :month_field, :week_field, :url_field,
16
- :email_field, :number_field, :range_field]
14
+ FIELDS_TO_SIGN.delete_if { |e| !::ActionView::Helpers::FormBuilder.instance_methods.include?(e) }
15
+ FIELDS_TO_SIGN.freeze
17
16
 
18
- FIELDS_TO_SIGN.delete_if { |e| !::ActionView::Helpers::FormBuilder.instance_methods.include?(e) }
19
- FIELDS_TO_SIGN.freeze
20
-
21
- FIELDS_TO_SIGN.each do |h|
22
- define_method(h) do |field, *args|
23
- add_signed_fields field
24
- super(field, *args)
25
- end
17
+ FIELDS_TO_SIGN.each do |h|
18
+ define_method(h) do |field, *args|
19
+ add_signed_fields field
20
+ super(field, *args)
26
21
  end
22
+ end
27
23
 
28
- def initialize(*)
29
- super
30
- if options[:signed_attributes_object]
31
- self.signed_attributes_object = options[:signed_attributes_object]
32
- else
33
- self.signed_attributes = { object_name => [] }
34
- self.signed_attributes_object = signed_attributes[object_name]
35
- end
24
+ BUILDERS = Hash.new do |h,k|
25
+ h[k] = Class.new(k) do
26
+ include FormBuilder
36
27
  end
28
+ end
37
29
 
38
- def form_signature_tag
39
- signed_attributes.each { |k,v| v.uniq! if v.is_a?(Array) }
40
- signed_attributes[:__options__] = { method: options[:html][:method], url: options[:url] } if options[:sign_destination]
41
- encoded_data = Base64.strict_encode64 Marshal.dump(signed_attributes)
42
- signature = SignedForm::HMAC.create_hmac(encoded_data)
43
- token = "#{encoded_data}--#{signature}"
44
- %(<input type="hidden" name="form_signature" value="#{token}" />\n).html_safe
30
+ def initialize(*)
31
+ super
32
+ if options[:signed_attributes_context]
33
+ @signed_attributes_context = options[:signed_attributes_context]
34
+ else
35
+ @signed_attributes = { object_name => [] }
36
+ @signed_attributes_context = @signed_attributes[object_name]
37
+ prepare_signed_attributes_hash
45
38
  end
39
+ end
46
40
 
47
- # Wrapper for Rails fields_for
48
- #
49
- # @see http://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-fields_for
50
- def fields_for(record_name, record_object = nil, fields_options = {}, &block)
51
- hash = {}
52
- array = []
41
+ def form_signature_tag
42
+ @signed_attributes.each { |k,v| v.uniq! if v.is_a?(Array) }
43
+ encoded_data = Base64.strict_encode64 Marshal.dump(@signed_attributes)
53
44
 
54
- if nested_attributes_association?(record_name)
55
- hash["#{record_name}_attributes"] = fields_options[:signed_attributes_object] = array
56
- else
57
- hash[record_name] = fields_options[:signed_attributes_object] = array
58
- end
45
+ hmac = SignedForm::HMAC.new(secret_key: SignedForm.secret_key)
46
+ signature = hmac.create(encoded_data)
47
+ token = "#{encoded_data}--#{signature}"
48
+ %(<input type="hidden" name="form_signature" value="#{token}" />\n).html_safe
49
+ end
59
50
 
60
- add_signed_fields hash
51
+ # Wrapper for Rails fields_for
52
+ #
53
+ # @see http://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-fields_for
54
+ def fields_for(record_name, record_object = nil, fields_options = {}, &block)
55
+ hash = {}
56
+ array = []
61
57
 
62
- content = super
63
- array.uniq!
64
- content
58
+ if nested_attributes_association?(record_name)
59
+ hash["#{record_name}_attributes"] = fields_options[:signed_attributes_context] = array
60
+ else
61
+ hash[record_name] = fields_options[:signed_attributes_context] = array
65
62
  end
66
63
 
67
- # This method is used to add additional fields to sign. A usecase for this may be if you want to add fields later with javascript.
68
- #
69
- # @example
70
- # <%= signed_form_for(@user) do |f| %>
71
- # <% f.add_signed_fields :name, :address
72
- # <% end %>
73
- #
74
- def add_signed_fields(*fields)
75
- signed_attributes_object.push(*fields)
76
- end
64
+ add_signed_fields hash
77
65
 
78
- private
66
+ content = super
67
+ array.uniq!
68
+ content
69
+ end
79
70
 
80
- attr_accessor :signed_attributes, :signed_attributes_object
71
+ # This method is used to add additional fields to sign. A usecase for this may be if you want to add fields later with javascript.
72
+ #
73
+ # @example
74
+ # <%= signed_form_for(@user) do |f| %>
75
+ # <% f.add_signed_fields :name, :address
76
+ # <% end %>
77
+ #
78
+ def add_signed_fields(*fields)
79
+ @signed_attributes_context.push(*fields)
80
+ options[:digest] << @template if options[:digest]
81
81
  end
82
82
 
83
- include Methods
83
+ private
84
+
85
+ def prepare_signed_attributes_hash
86
+ @signed_attributes[:_options_] = {}
87
+
88
+ if options[:sign_destination]
89
+ @signed_attributes[:_options_][:method] = options[:html][:method]
90
+ @signed_attributes[:_options_][:url] = options[:url]
91
+ end
92
+
93
+ if options[:digest]
94
+ @signed_attributes[:_options_][:digest] = options[:digest] = Digestor.new(@template)
95
+ @signed_attributes[:_options_][:digest_expiration] = Time.now + options[:digest_grace_period] if options[:digest_grace_period]
96
+ end
97
+ end
84
98
  end
85
99
  end
@@ -0,0 +1,50 @@
1
+ module SignedForm
2
+ class GateKeeper
3
+ attr_reader :allowed_attributes
4
+
5
+ def initialize(controller)
6
+ @controller = controller
7
+ @params = controller.params
8
+ @request = controller.request
9
+
10
+ extract_and_verify_form_signature
11
+ verify_destination
12
+ verify_digest
13
+ end
14
+
15
+ def options
16
+ @options ||= {}
17
+ end
18
+
19
+ def extract_and_verify_form_signature
20
+ data, signature = @params['form_signature'].split('--', 2)
21
+ hmac = SignedForm::HMAC.new secret_key: SignedForm.secret_key
22
+
23
+ signature ||= ''
24
+
25
+ raise Errors::InvalidSignature, "Form signature is not valid" unless hmac.verify signature, data
26
+
27
+ @allowed_attributes = Marshal.load Base64.strict_decode64(data)
28
+ @options = allowed_attributes.delete(:_options_)
29
+ end
30
+
31
+ def verify_destination
32
+ return unless options[:method] && options[:url]
33
+ raise Errors::InvalidURL if options[:method].to_s.casecmp(@request.request_method) != 0
34
+ url = @controller.url_for(options[:url])
35
+ raise Errors::InvalidURL if url != @request.fullpath && url != @request.url
36
+ end
37
+
38
+ def verify_digest
39
+ return unless options[:digest]
40
+
41
+ return if options[:digest_expiration] && Time.now < options[:digest_expiration]
42
+
43
+ digestor = options[:digest]
44
+ given_digest = digestor.to_s
45
+ digestor.view_paths = @controller.view_paths.map(&:to_s)
46
+ digestor.refresh
47
+ raise Errors::ExpiredForm unless given_digest == digestor.to_s
48
+ end
49
+ end
50
+ end
@@ -1,24 +1,22 @@
1
- require 'openssl'
1
+ require "openssl"
2
2
 
3
3
  module SignedForm
4
- module HMAC
5
- extend self
6
-
4
+ class HMAC
7
5
  attr_accessor :secret_key
8
6
 
9
- def create_hmac(data)
7
+ def initialize(options = {})
8
+ self.secret_key = options[:secret_key]
9
+
10
10
  if secret_key.nil? || secret_key.empty?
11
11
  raise Errors::NoSecretKey, "Please consult the README for instructions on creating a secret key"
12
12
  end
13
+ end
13
14
 
15
+ def create(data)
14
16
  OpenSSL::HMAC.hexdigest OpenSSL::Digest::SHA1.new, secret_key, data
15
17
  end
16
18
 
17
- def verify_hmac(signature, data)
18
- if secret_key.nil? || secret_key.empty?
19
- raise Errors::NoSecretKey, "Please consult the README for instructions on creating a secret key"
20
- end
21
-
19
+ def verify(signature, data)
22
20
  secure_compare OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret_key, data), signature
23
21
  end
24
22
 
@@ -0,0 +1,17 @@
1
+ module SignedForm
2
+ module TestHelper
3
+ def permit_all_parameters
4
+ @controller.instance_eval do
5
+ def params
6
+ @permit_all_parameters ||= super.permit!
7
+ end
8
+ end
9
+
10
+ if block_given?
11
+ yield
12
+ @controller.remove_instance_variable :@permit_all_parameters
13
+ @controller.singleton_class.send :remove_method, :params
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,7 +1,7 @@
1
1
  module SignedForm
2
2
  MAJOR = 0
3
- MINOR = 1
4
- PATCH = 2
3
+ MINOR = 2
4
+ PATCH = 0
5
5
  PRE = nil
6
6
 
7
7
  VERSION = [MAJOR, MINOR, PATCH, PRE].compact.join '.'
data/signed_form.gemspec CHANGED
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.add_development_dependency "rake"
23
23
  spec.add_development_dependency "rspec", "~> 2.13"
24
24
  spec.add_development_dependency "activemodel", ">= 3.1"
25
+ spec.add_development_dependency "coveralls"
25
26
 
26
27
  spec.add_dependency "actionpack", ">= 3.1"
27
28
 
@@ -0,0 +1,81 @@
1
+ require 'spec_helper'
2
+
3
+ describe SignedForm::Digestor do
4
+ let(:view_paths) { [File.join(File.dirname(__FILE__), 'fixtures', 'views')] }
5
+ let(:template) { double(view_paths: view_paths) }
6
+
7
+ before do
8
+ def template.virtual_path=(path)
9
+ @virtual_path = path
10
+ end
11
+
12
+ template.virtual_path = 'form'
13
+ end
14
+
15
+ it "should raise if template doesn't have view_paths" do
16
+ template = double(view_paths: -> { raise NoMethodError })
17
+ expect { SignedForm::Digestor.new(template) }.to raise_error(SignedForm::Errors::UnableToDigest)
18
+ end
19
+
20
+ it "should raise if the template doesn't have a virtual path" do
21
+ template = double(view_paths: view_paths)
22
+ expect { SignedForm::Digestor.new(template) }.to raise_error(SignedForm::Errors::UnableToDigest)
23
+
24
+ template.instance_variable_set(:@virtual_path, 'form')
25
+ digestor = SignedForm::Digestor.new(template)
26
+ template.instance_variable_set(:@virtual_path, nil)
27
+ expect { digestor << template }.to raise_error(SignedForm::Errors::UnableToDigest)
28
+ end
29
+
30
+ it "should not marshal view paths" do
31
+ digestor = SignedForm::Digestor.new(template)
32
+
33
+ digestor.view_paths.should_not be_empty
34
+
35
+ data = Marshal.dump digestor
36
+ digestor = Marshal.load data
37
+
38
+ digestor.view_paths.should be_empty
39
+ end
40
+
41
+ specify "#to_s should return the correct MD5 digest" do
42
+ digestor = SignedForm::Digestor.new(template)
43
+ digestor.to_s.should == "7a956713f33cabd57357c70025109e69"
44
+ template.virtual_path = "_fields"
45
+ digestor << template
46
+ digestor.to_s.should == "6a161ab9978322e8251d809b3558ab1a"
47
+ end
48
+
49
+ specify "The view order should not affect the digest" do
50
+ digestor = SignedForm::Digestor.new(template)
51
+ template.virtual_path = '_fields'
52
+ digestor << template
53
+
54
+ digestor2 = SignedForm::Digestor.new(template)
55
+ template.virtual_path = 'form'
56
+ digestor2 << template
57
+ digestor.to_s.should == digestor2.to_s
58
+ end
59
+
60
+ it "should marshal and taint the digest" do
61
+ digestor = SignedForm::Digestor.new(template)
62
+ data = Marshal.dump digestor
63
+ digestor = Marshal.load data
64
+ digestor.to_s.should be_tainted
65
+ end
66
+
67
+ it "should reset the digest if a template is added" do
68
+ digestor = SignedForm::Digestor.new(template)
69
+ first_digest = digestor.to_s
70
+ template.virtual_path = '_fields'
71
+ digestor << template
72
+ digestor.to_s.should_not == first_digest
73
+ end
74
+
75
+ it "should be idempotent" do
76
+ digestor = SignedForm::Digestor.new(template)
77
+ first_digest = digestor.to_s
78
+ digestor << template
79
+ digestor.to_s.should == first_digest
80
+ end
81
+ end