active_hashcash 0.3.2 → 0.5.0

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.
@@ -1,3 +1,3 @@
1
1
  module ActiveHashcash
2
- VERSION = "0.3.2"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -1,6 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_hashcash/version"
2
4
  require "active_hashcash/engine"
3
5
 
6
+ # ActiveHashcash protects Rails applications against bots and brute force attacks without annoying humans.
7
+ # See the rdoc-ref:README.md for more explanations about Hashcash.
8
+ #
9
+ # Include this module into your Rails controller.
10
+ #
11
+ # class SessionController < ApplicationController
12
+ # include ActiveHashcash
13
+ #
14
+ # before_action :check_hashcash, only: :create
15
+ # end
16
+ #
17
+ # Your are welcome to override most of the methods to customize to your needs.
18
+ # For example, if your app runs behind a loab balancer you should probably override #hashcash_ip_address.
19
+ #
4
20
  module ActiveHashcash
5
21
  extend ActiveSupport::Concern
6
22
 
@@ -16,7 +32,14 @@ module ActiveHashcash
16
32
 
17
33
  mattr_accessor :date_format, instance_accessor: false, default: "%y%m%d"
18
34
 
19
- # Call me via a before_action when the form is submitted : `before_action :check_hashcash, only: :create`
35
+ mattr_accessor :base_controller_class, default: "ActionController::Base"
36
+
37
+ # Call that method via a before_action when the form is submitted:
38
+ #
39
+ # before_action :check_hashcash, only: :create
40
+ #
41
+ # In case of invalid hashcash it calls hashcash_after_failure that you can override.
42
+ # Otherwise, hashcash stamp is stored in database to prevent from double spending.
20
43
  def check_hashcash
21
44
  attrs = {
22
45
  ip_address: hashcash_ip_address,
@@ -30,21 +53,26 @@ module ActiveHashcash
30
53
  end
31
54
  end
32
55
 
33
- # Override the methods below in your controller, to change any parameter or behaviour.
34
-
56
+ # Returns remote IP address.
57
+ # They are used to automatically increase complexity when the same IP sends many valid hashcash.
58
+ # If you're app is behind a load balancer, you should probably override it to read the right HTTP header.
35
59
  def hashcash_ip_address
36
60
  request.remote_ip
37
61
  end
38
62
 
63
+ # Return current request path to be saved to the sucessful ActiveHash::Stamp.
64
+ # If multiple forms are protected via hashcash this is an interesting info.
39
65
  def hashcash_request_path
40
66
  request.path
41
67
  end
42
68
 
69
+ # Override this method to store custom data for each stamp.
70
+ # It must returns a hash or nil.
43
71
  def hashcash_stamp_context
44
- # Override this method to store custom data for each stamp
45
72
  end
46
73
 
47
- # By default the host name is used as the resource.
74
+ # This is the resource used to build the hashcash stamp.
75
+ # By default the host name is returned.
48
76
  # It' should be good for most cases and prevent from reusing the same stamp between sites.
49
77
  def hashcash_resource
50
78
  ActiveHashcash.resource || request.host
@@ -52,7 +80,7 @@ module ActiveHashcash
52
80
 
53
81
  # Returns the complexity, the higher the slower it is.
54
82
  # Complexity is increased logarithmicly for each IP during the last 24H to slowdown brute force attacks.
55
- # The minimun value returned is `ActiveHashcash.bits`.
83
+ # The minimun value returned is ActiveHashcash.bits.
56
84
  def hashcash_bits
57
85
  if (previous_stamp_count = ActiveHashcash::Stamp.where(ip_address: hashcash_ip_address).where(created_at: 1.day.ago..).count) > 0
58
86
  (ActiveHashcash.bits + Math.log2(previous_stamp_count)).floor
@@ -71,7 +99,9 @@ module ActiveHashcash
71
99
  t("active_hashcash.waiting_label")
72
100
  end
73
101
 
74
- # Override to provide a different behaviour when hashcash failed
102
+ # That method is called when #check_hashcash fails.
103
+ # It raises ActionController::InvalidAuthenticityToken so HTTP response will be 422 by default.
104
+ # Override this method to provide a different behaviour.
75
105
  def hashcash_after_failure
76
106
  raise ActionController::InvalidAuthenticityToken.new("Invalid hashcash #{hashcash_param}")
77
107
  end
@@ -83,8 +113,19 @@ module ActiveHashcash
83
113
 
84
114
  # Call it inside the form that have to be protected and don't forget to initialize the JavaScript Hascash.setup().
85
115
  # Unless you need something really special, you should not need to override this method.
116
+ #
117
+ # <% form_for model do |form| %>
118
+ # <%= hashcash_hidden_field_tag %>
119
+ #
120
+ # <% end %>
121
+ #
86
122
  def hashcash_hidden_field_tag(name = :hashcash)
87
- options = {resource: hashcash_resource, bits: hashcash_bits, waiting_message: hashcash_waiting_message}
123
+ options = {
124
+ resource: hashcash_resource,
125
+ bits: hashcash_bits,
126
+ waiting_message: hashcash_waiting_message,
127
+ date: Date.today.strftime("%y%m%d")
128
+ }
88
129
  view_context.hidden_field_tag(name, "", "data-hashcash" => options.to_json)
89
130
  end
90
131
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_hashcash
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexis Bernard
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-08-29 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rails
@@ -62,6 +61,7 @@ files:
62
61
  - app/views/active_hashcash/stamps/index.html.erb
63
62
  - app/views/active_hashcash/stamps/show.html.erb
64
63
  - app/views/layouts/active_hashcash/application.html.erb
64
+ - config/locales/ca.yml
65
65
  - config/locales/de.yml
66
66
  - config/locales/en.yml
67
67
  - config/locales/es.yml
@@ -74,7 +74,6 @@ files:
74
74
  - lib/active_hashcash.rb
75
75
  - lib/active_hashcash/engine.rb
76
76
  - lib/active_hashcash/version.rb
77
- - lib/hashcash.js
78
77
  - lib/tasks/active_hashcash_tasks.rake
79
78
  homepage: https://github.com/BaseSecrete/active_hashcash
80
79
  licenses:
@@ -83,7 +82,6 @@ metadata:
83
82
  homepage_uri: https://github.com/BaseSecrete/active_hashcash
84
83
  source_code_uri: https://github.com/BaseSecrete/active_hashcash
85
84
  changelog_uri: https://github.com/BaseSecrete/active_hashcash/CHANGELOG.md
86
- post_install_message:
87
85
  rdoc_options: []
88
86
  require_paths:
89
87
  - lib
@@ -98,8 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
98
96
  - !ruby/object:Gem::Version
99
97
  version: '0'
100
98
  requirements: []
101
- rubygems_version: 3.5.9
102
- signing_key:
99
+ rubygems_version: 4.0.10
103
100
  specification_version: 4
104
101
  summary: Protect Rails applications against bots and brute force attacks without annoying
105
102
  humans.
data/lib/hashcash.js DELETED
@@ -1,257 +0,0 @@
1
- // http://www.hashcash.org/docs/hashcash.html
2
- // <input type="hiden" name="hashcash" data-hashcash="{resource: 'site.example', bits: 16}"/>
3
- Hashcash = function(input) {
4
- options = JSON.parse(input.getAttribute("data-hashcash"))
5
- Hashcash.disableParentForm(input, options)
6
- input.dispatchEvent(new CustomEvent("hashcash:mint", {bubbles: true}))
7
-
8
- Hashcash.mint(options.resource, options, function(stamp) {
9
- input.value = stamp.toString()
10
- Hashcash.enableParentForm(input, options)
11
- input.dispatchEvent(new CustomEvent("hashcash:minted", {bubbles: true, detail: {stamp: stamp}}))
12
- })
13
- }
14
-
15
- Hashcash.setup = function() {
16
- if (document.readyState != "loading") {
17
- var input = document.querySelector("input#hashcash")
18
- input && new Hashcash(input)
19
- } else
20
- document.addEventListener("DOMContentLoaded", Hashcash.setup )
21
- }
22
-
23
- Hashcash.disableParentForm = function(input, options) {
24
- input.form.querySelectorAll("[type=submit]").forEach(function(submit) {
25
- submit.originalValue = submit.value
26
- options["waiting_message"] && (submit.value = options["waiting_message"])
27
- submit.disabled = true
28
- })
29
- }
30
-
31
- Hashcash.enableParentForm = function(input, options) {
32
- input.form.querySelectorAll("[type=submit]").forEach(function(submit) {
33
- submit.originalValue && (submit.value = submit.originalValue)
34
- submit.disabled = null
35
- })
36
- }
37
-
38
- Hashcash.default = {
39
- version: 1,
40
- bits: 20,
41
- extension: null,
42
- }
43
-
44
- Hashcash.mint = function(resource, options, callback) {
45
- // Format date to YYMMDD
46
- var date = new Date
47
- var year = date.getFullYear().toString()
48
- year = year.slice(year.length - 2, year.length)
49
- var month = (date.getMonth() + 1).toString().padStart(2, "0")
50
- var day = date.getDate().toString().padStart(2, "0")
51
-
52
- var stamp = new Hashcash.Stamp(
53
- options.version || Hashcash.default.version,
54
- options.bits || Hashcash.default.bits,
55
- options.date || year + month + day,
56
- resource,
57
- options.extension || Hashcash.default.extension,
58
- options.rand || Math.random().toString(36).substr(2, 10),
59
- )
60
- return stamp.work(callback)
61
- }
62
-
63
- Hashcash.Stamp = function(version, bits, date, resource, extension, rand, counter = 0) {
64
- this.version = version
65
- this.bits = bits
66
- this.date = date
67
- this.resource = resource
68
- this.extension = extension
69
- this.rand = rand
70
- this.counter = counter
71
- }
72
-
73
- Hashcash.Stamp.parse = function(string) {
74
- var args = string.split(":")
75
- return new Hashcash.Stamp(args[0], args[1], args[2], args[3], args[4], args[5], args[6])
76
- }
77
-
78
- Hashcash.Stamp.prototype.toString = function() {
79
- return [this.version, this.bits, this.date, this.resource, this.extension, this.rand, this.counter].join(":")
80
- }
81
-
82
- // Trigger the given callback when the problem is solved.
83
- // In order to not freeze the page, setTimeout is called every 100ms to let some CPU to other tasks.
84
- Hashcash.Stamp.prototype.work = function(callback) {
85
- this.startClock()
86
- var timer = performance.now()
87
- while (!this.check())
88
- if (this.counter++ && performance.now() - timer > 100)
89
- return setTimeout(this.work.bind(this), 0, callback)
90
- this.stopClock()
91
- callback(this)
92
- }
93
-
94
- Hashcash.Stamp.prototype.check = function() {
95
- var array = Hashcash.sha1(this.toString())
96
- return array[0] >> (160-this.bits) == 0
97
- }
98
-
99
- Hashcash.Stamp.prototype.startClock = function() {
100
- this.startedAt || (this.startedAt = performance.now())
101
- }
102
-
103
- Hashcash.Stamp.prototype.stopClock = function() {
104
- this.endedAt || (this.endedAt = performance.now())
105
- var duration = this.endedAt - this.startedAt
106
- var speed = Math.round(this.counter * 1000 / duration)
107
- console.debug("Hashcash " + this.toString() + " minted in " + duration + "ms (" + speed + " per seconds)")
108
- }
109
-
110
- /**
111
- * Secure Hash Algorithm (SHA1)
112
- * http://www.webtoolkit.info/
113
- **/
114
- Hashcash.sha1 = function(msg) {
115
- var rotate_left = Hashcash.sha1.rotate_left
116
- var Utf8Encode = Hashcash.sha1.Utf8Encode
117
-
118
- var blockstart;
119
- var i, j;
120
- var W = new Array(80);
121
- var H0 = 0x67452301;
122
- var H1 = 0xEFCDAB89;
123
- var H2 = 0x98BADCFE;
124
- var H3 = 0x10325476;
125
- var H4 = 0xC3D2E1F0;
126
- var A, B, C, D, E;
127
- var temp;
128
- msg = Utf8Encode(msg);
129
- var msg_len = msg.length;
130
- var word_array = new Array();
131
- for (i = 0; i < msg_len - 3; i += 4) {
132
- j = msg.charCodeAt(i) << 24 | msg.charCodeAt(i + 1) << 16 |
133
- msg.charCodeAt(i + 2) << 8 | msg.charCodeAt(i + 3);
134
- word_array.push(j);
135
- }
136
- switch (msg_len % 4) {
137
- case 0:
138
- i = 0x080000000;
139
- break;
140
- case 1:
141
- i = msg.charCodeAt(msg_len - 1) << 24 | 0x0800000;
142
- break;
143
- case 2:
144
- i = msg.charCodeAt(msg_len - 2) << 24 | msg.charCodeAt(msg_len - 1) << 16 | 0x08000;
145
- break;
146
- case 3:
147
- i = msg.charCodeAt(msg_len - 3) << 24 | msg.charCodeAt(msg_len - 2) << 16 | msg.charCodeAt(msg_len - 1) << 8 | 0x80;
148
- break;
149
- }
150
- word_array.push(i);
151
- while ((word_array.length % 16) != 14) word_array.push(0);
152
- word_array.push(msg_len >>> 29);
153
- word_array.push((msg_len << 3) & 0x0ffffffff);
154
- for (blockstart = 0; blockstart < word_array.length; blockstart += 16) {
155
- for (i = 0; i < 16; i++) W[i] = word_array[blockstart + i];
156
- for (i = 16; i <= 79; i++) W[i] = rotate_left(W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16], 1);
157
- A = H0;
158
- B = H1;
159
- C = H2;
160
- D = H3;
161
- E = H4;
162
- for (i = 0; i <= 19; i++) {
163
- temp = (rotate_left(A, 5) + ((B & C) | (~B & D)) + E + W[i] + 0x5A827999) & 0x0ffffffff;
164
- E = D;
165
- D = C;
166
- C = rotate_left(B, 30);
167
- B = A;
168
- A = temp;
169
- }
170
- for (i = 20; i <= 39; i++) {
171
- temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1) & 0x0ffffffff;
172
- E = D;
173
- D = C;
174
- C = rotate_left(B, 30);
175
- B = A;
176
- A = temp;
177
- }
178
- for (i = 40; i <= 59; i++) {
179
- temp = (rotate_left(A, 5) + ((B & C) | (B & D) | (C & D)) + E + W[i] + 0x8F1BBCDC) & 0x0ffffffff;
180
- E = D;
181
- D = C;
182
- C = rotate_left(B, 30);
183
- B = A;
184
- A = temp;
185
- }
186
- for (i = 60; i <= 79; i++) {
187
- temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6) & 0x0ffffffff;
188
- E = D;
189
- D = C;
190
- C = rotate_left(B, 30);
191
- B = A;
192
- A = temp;
193
- }
194
- H0 = (H0 + A) & 0x0ffffffff;
195
- H1 = (H1 + B) & 0x0ffffffff;
196
- H2 = (H2 + C) & 0x0ffffffff;
197
- H3 = (H3 + D) & 0x0ffffffff;
198
- H4 = (H4 + E) & 0x0ffffffff;
199
- }
200
- return [H0, H1, H2, H3, H4]
201
- }
202
-
203
- Hashcash.hexSha1 = function(msg) {
204
- var array = Hashcash.sha1(msg)
205
- var cvt_hex = Hashcash.sha1.cvt_hex
206
- return cvt_hex(array[0]) + cvt_hex(array[1]) + cvt_hex(array[2]) + cvt_hex(array3) + cvt_hex(array[4])
207
- }
208
-
209
- Hashcash.sha1.rotate_left = function(n, s) {
210
- var t4 = (n << s) | (n >>> (32 - s));
211
- return t4;
212
- };
213
-
214
- Hashcash.sha1.lsb_hex = function(val) {
215
- var str = '';
216
- var i;
217
- var vh;
218
- var vl;
219
- for (i = 0; i <= 6; i += 2) {
220
- vh = (val >>> (i * 4 + 4)) & 0x0f;
221
- vl = (val >>> (i * 4)) & 0x0f;
222
- str += vh.toString(16) + vl.toString(16);
223
- }
224
- return str;
225
- };
226
-
227
- Hashcash.sha1.cvt_hex = function(val) {
228
- var str = '';
229
- var i;
230
- var v;
231
- for (i = 7; i >= 0; i--) {
232
- v = (val >>> (i * 4)) & 0x0f;
233
- str += v.toString(16);
234
- }
235
- return str;
236
- };
237
-
238
- Hashcash.sha1.Utf8Encode = function(string) {
239
- string = string.replace(/\r\n/g, '\n');
240
- var utftext = '';
241
- for (var n = 0; n < string.length; n++) {
242
- var c = string.charCodeAt(n);
243
- if (c < 128) {
244
- utftext += String.fromCharCode(c);
245
- } else if ((c > 127) && (c < 2048)) {
246
- utftext += String.fromCharCode((c >> 6) | 192);
247
- utftext += String.fromCharCode((c & 63) | 128);
248
- } else {
249
- utftext += String.fromCharCode((c >> 12) | 224);
250
- utftext += String.fromCharCode(((c >> 6) & 63) | 128);
251
- utftext += String.fromCharCode((c & 63) | 128);
252
- }
253
- }
254
- return utftext;
255
- };
256
-
257
- Hashcash.setup()