active_hashcash 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +101 -14
- data/app/assets/config/active_hashcash_manifest.js +1 -0
- data/app/assets/javascripts/hashcash.js +257 -0
- data/app/assets/stylesheets/active_hashcash/application.css +15 -0
- data/app/controllers/active_hashcash/addresses_controller.rb +7 -0
- data/app/controllers/active_hashcash/application_controller.rb +4 -0
- data/app/controllers/active_hashcash/assets_controller.rb +34 -0
- data/app/controllers/active_hashcash/stamps_controller.rb +11 -0
- data/app/helpers/active_hashcash/addresses_helper.rb +4 -0
- data/app/helpers/active_hashcash/application_helper.rb +4 -0
- data/app/helpers/active_hashcash/stamps_helper.rb +4 -0
- data/app/jobs/active_hashcash/application_job.rb +4 -0
- data/app/mailers/active_hashcash/application_mailer.rb +6 -0
- data/app/models/active_hashcash/application_record.rb +5 -0
- data/app/models/active_hashcash/stamp.rb +70 -0
- data/app/views/active_hashcash/addresses/index.html.erb +17 -0
- data/app/views/active_hashcash/assets/_logo.svg.erb +1 -0
- data/app/views/active_hashcash/assets/_style.css +148 -0
- data/app/views/active_hashcash/assets/application.css.erb +1 -0
- data/app/views/active_hashcash/assets/ariato.css.erb +2 -0
- data/app/views/active_hashcash/assets/favicon.ico +0 -0
- data/app/views/active_hashcash/assets/favicon.svg.erb +1 -0
- data/app/views/active_hashcash/assets/vendor/_ariato_base.css +1297 -0
- data/app/views/active_hashcash/assets/vendor/_ariato_extra.css +1206 -0
- data/app/views/active_hashcash/stamps/_filters.html.erb +39 -0
- data/app/views/active_hashcash/stamps/index.html.erb +25 -0
- data/app/views/active_hashcash/stamps/show.html.erb +21 -0
- data/app/views/layouts/active_hashcash/application.html.erb +36 -0
- data/config/locales/de.yml +4 -0
- data/config/locales/en.yml +4 -0
- data/config/locales/es.yml +4 -0
- data/config/locales/fr.yml +4 -0
- data/config/locales/it.yml +4 -0
- data/config/locales/jp.yml +4 -0
- data/config/locales/pt.yml +4 -0
- data/config/routes.rb +6 -0
- data/db/migrate/20240215143453_create_active_hashcash_stamps.rb +25 -0
- data/lib/active_hashcash/engine.rb +2 -14
- data/lib/active_hashcash/version.rb +1 -1
- data/lib/active_hashcash.rb +35 -21
- data/lib/tasks/active_hashcash_tasks.rake +4 -0
- metadata +47 -26
- data/lib/active_hashcash/stamp.rb +0 -52
- data/lib/active_hashcash/store.rb +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dbc710b7b2cc6fef0667915ac1a95ccfdd6d1cb3eaa1fa9b97d29ef81e79985d
|
4
|
+
data.tar.gz: 323c22e60aa27d2d87e39ee8f5c356d2fc6e8190426e49831a29d762d2e74f28
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed74328b7b1f91eb2dece7a29efb5b9d68079872694e589caca3e79de1a06e6d0bd5d2556dcab12f92fcede4c08b075a4dc4b9077e79c8511cb4045ce509c82c
|
7
|
+
data.tar.gz: e8f580f0668529b3bd7e220f2bdbd4b2eae6bc63272228b9cb4d1fe75ef0e30202983e3e06d91664558d36239137b69d7ba96996c80b370f4b1638300f77781a
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
+
# Changelog of ActiveHashcash
|
2
|
+
|
3
|
+
## Unrelease
|
4
|
+
|
5
|
+
- Fix gem spec list files
|
6
|
+
|
7
|
+
## 0.3.0 - 2024-03-14
|
8
|
+
|
9
|
+
- Increase complexity automatically to slowdown brute force attacks
|
10
|
+
- Add mountable dashboard to list latest stamps and most frequent IP addresses
|
11
|
+
- Store stamps into the database instead of Redis
|
12
|
+
- Fix ActiveHashcash::Store#add? by converting stamp to a string
|
13
|
+
|
1
14
|
## 0.2.0 - 2022-08-02
|
2
15
|
|
3
16
|
- Add ActiveHashcash::Store#clean to removed expired stamps
|
data/README.md
CHANGED
@@ -2,29 +2,34 @@
|
|
2
2
|
|
3
3
|
<img align="right" width="200px" src="logo.png" alt="Active Hashcash logo"/>
|
4
4
|
|
5
|
-
ActiveHashcash protects
|
5
|
+
ActiveHashcash protects Rails applications against bots and brute force attacks without annoying humans.
|
6
6
|
|
7
7
|
Hashcash is proof-of-work algorithm, invented by Adam Back in 1997, to protect systems against denial of service attacks.
|
8
|
-
ActiveHashcash is an easy way to protect any Rails application against brute force attacks and
|
8
|
+
ActiveHashcash is an easy way to protect any Rails application against brute force attacks and bots.
|
9
9
|
|
10
10
|
The idea is to force clients to spend some time to solve a hard problem that is very easy to verify for the server.
|
11
11
|
We have developped ActiveHashcash after seeing brute force attacks against our Rails application monitoring service [RorVsWild](https://rorvswild.com).
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
The
|
13
|
+
ActiveHashcash is ideal to set up on sensitive forms such as login and registration.
|
14
|
+
While the user is filling the form, the problem is solved in JavaScript and set the result into a hidden input text.
|
15
|
+
The form cannot be submitted while the proof of work has not been found.
|
16
|
+
Then the user submits the form, and the stamp is verified by the controller in a before action.
|
16
17
|
|
17
|
-
It blocks bots that do not interpret JavaScript since the proof of work is not computed.
|
18
|
+
It blocks bots that do not interpret JavaScript since the proof of work is not computed.
|
19
|
+
More sophisticated bots and brute force attacks are slow down.
|
20
|
+
Moreover the complexity increases automatically for IP addresses sending many requests.
|
21
|
+
Thus it becomes very CPU costly for attackers.
|
18
22
|
|
19
|
-
|
23
|
+
Finally legitimate users are not annoyed by asking to solve a puzzle or clicking on the all images containing a bus.
|
24
|
+
Here is a [demo on a registration form](https://www.rorvswild.com/session) :
|
20
25
|
|
21
26
|

|
22
27
|
|
23
|
-
|
28
|
+
---
|
24
29
|
|
25
|
-
|
26
|
-
|
27
|
-
|
30
|
+
<img align="left" height="24px" src="rorvswild_logo.jpg" alt="RorVsWild logo"/>Made by <a href="https://www.rorvswild.com">RorVsWild</a>, performances & exceptions monitoring for Ruby on Rails applications.
|
31
|
+
|
32
|
+
---
|
28
33
|
|
29
34
|
## Installation
|
30
35
|
|
@@ -62,26 +67,108 @@ end
|
|
62
67
|
To customize some behaviour, you can override most of the methods which begins with `hashcash_`.
|
63
68
|
Simply have a look to `active_hashcash.rb`.
|
64
69
|
|
70
|
+
Stamps are stored into into the database to prevents from spending them more than once.
|
71
|
+
You must run a migration:
|
72
|
+
|
73
|
+
```
|
74
|
+
rails active_hashcash:install:migrations
|
75
|
+
rails db:migrate
|
76
|
+
```
|
77
|
+
|
78
|
+
### Dashboard
|
79
|
+
|
80
|
+
There is a mountable dahsboard which allows to see all spent stamps.
|
81
|
+
It's not mandatory, but useful for monitoring purpose.
|
82
|
+
|
83
|
+

|
84
|
+
|
85
|
+
```ruby
|
86
|
+
# config/routes.rb
|
87
|
+
mount ActiveHashcash::Engine, at: "hashcash"
|
88
|
+
```
|
89
|
+
|
90
|
+
ActiveHashcash cannot guess how you handle user authentication, because it is different for all Rails applications.
|
91
|
+
So you have to monkey patch `ActiveHashcash::ApplicationController` in order to inject your own mechanism.
|
92
|
+
The patch can be saved wherever you want.
|
93
|
+
For example, I like to have all the patches in one place, so I put them in `lib/patches`.
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
# lib/patches/active_hashcash.rb
|
97
|
+
|
98
|
+
ActiveHashcash::ApplicationController.class_eval do
|
99
|
+
before_action :require_admin
|
100
|
+
|
101
|
+
def require_admin
|
102
|
+
# This example supposes there are current_user and User#admin? methods
|
103
|
+
raise ActionController::RoutingError.new("Not found") unless current_user.try(:admin?)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
Then you have to require the monkey patch.
|
110
|
+
Because it's loaded via require, it won't be reloaded in development.
|
111
|
+
Since you are not supposed to change this file often, it should not be an issue.
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
# config/application.rb
|
115
|
+
config.after_initialize do
|
116
|
+
require "patches/active_hashcash"
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
If you use Devise, you can check the permission directly from routes.rb:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
# config/routes.rb
|
124
|
+
authenticate :user, -> (u) { u.admin? } do # Supposing there is a User#admin? method
|
125
|
+
mount ActiveHashcash::Engine, at: "hashcash" # http://localhost:3000/hashcash
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
### Before version 0.3.0
|
130
|
+
|
65
131
|
You must have Redis in order to prevent double spent stamps. Otherwise it will be useless.
|
66
132
|
It automatically tries to connect with the environement variables `ACTIVE_HASHCASH_REDIS_URL` or `REDIS_URL`.
|
67
133
|
You can also manually set the URL with `ActiveHashcash.redis_url = redis://user:password@localhost:6379`.
|
68
134
|
|
69
135
|
You should call `ActiveHashcash::Store#clean` once a day, to remove expired stamps.
|
70
136
|
|
137
|
+
To upgrade from 0.2.0 you must run the migration :
|
138
|
+
|
139
|
+
```
|
140
|
+
rails active_hashcash:install:migrations
|
141
|
+
rails db:migrate
|
142
|
+
```
|
143
|
+
|
71
144
|
## Complexity
|
72
145
|
|
73
146
|
Complexity is the most important parameter. By default its value is 20 and requires most of the time 5 to 20 seconds to be solved on a decent laptop.
|
74
147
|
The user won't wait that long, since he needs to fill the form while the problem is solving.
|
75
148
|
Howevever, if your application includes people with slow and old devices, then consider lowering this value, to 16 or 18.
|
76
149
|
|
77
|
-
You can change the complexity
|
150
|
+
You can change the minimum complexity with `ActiveHashcash.bits = 20`.
|
151
|
+
|
152
|
+
Since version 0.3.0, the complexity increases with the number of stamps spent during le last 24H from the same IP address.
|
153
|
+
Thus it becomes very efficient to slow down brute force attacks.
|
154
|
+
|
155
|
+
## Limitations
|
156
|
+
|
157
|
+
The JavaScript implementation is 10 to 20 times slower than the official C version.
|
158
|
+
I first used the SubtleCrypto API but it is surprisingly slower than a custom SHA1 implementation.
|
159
|
+
Maybe I did in an unefficient way 2df3ba5?
|
160
|
+
Another idea would be to compile the work algorithm in wasm.
|
161
|
+
|
162
|
+
Unfortunately, I'm not a JavaScript expert.
|
163
|
+
Maybe you have good JS skills to optimize it?
|
164
|
+
Any help would be appreciate to better fights bots and brute for attacks!
|
78
165
|
|
79
166
|
## Contributing
|
80
167
|
|
81
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
168
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/BaseSecrete/active_hashcash.
|
82
169
|
|
83
170
|
## License
|
84
171
|
|
85
172
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
86
173
|
|
87
|
-
Made by Alexis Bernard at [
|
174
|
+
Made by Alexis Bernard at [RorVsWild](https://www.rorvswild.com).
|
@@ -0,0 +1 @@
|
|
1
|
+
//= link_directory ../stylesheets/active_hashcash .css
|
@@ -0,0 +1,257 @@
|
|
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()
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module ActiveHashcash
|
2
|
+
class AssetsController < ApplicationController
|
3
|
+
protect_from_forgery except: :show
|
4
|
+
|
5
|
+
Mime::Type.register "image/x-icon", :ico
|
6
|
+
|
7
|
+
def show
|
8
|
+
if endpoints.include?(file_name = File.basename(request.path))
|
9
|
+
file_path = ActiveHashcash::Engine.root.join / "app/views/active_hashcash/assets" / file_name
|
10
|
+
if File.exists?("#{file_path}.erb")
|
11
|
+
render(params[:id], mime_type: mime_type)
|
12
|
+
else
|
13
|
+
render(file: file_path)
|
14
|
+
end
|
15
|
+
expires_in(1.day, public: true)
|
16
|
+
else
|
17
|
+
raise ActionController::RoutingError.new
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def endpoints
|
24
|
+
return @endpoints if @endpoints
|
25
|
+
folder = ActiveHashcash::Engine.root.join("app/views", controller_path)
|
26
|
+
files = folder.each_child.map { |path| File.basename(path).delete_suffix(".erb") }
|
27
|
+
@endpoints = files.delete_if { |str| str.start_with?("_") }
|
28
|
+
end
|
29
|
+
|
30
|
+
def mime_type
|
31
|
+
Mime::Type.lookup_by_extension(File.extname(request.path).delete_prefix("."))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveHashcash
|
4
|
+
class Stamp < ApplicationRecord
|
5
|
+
validates_presence_of :version, :bits, :date, :resource, :rand, :counter
|
6
|
+
|
7
|
+
scope :created_from, -> (date) { where(created_at: date..) }
|
8
|
+
scope :created_to, -> (date) { where(created_at: ..date) }
|
9
|
+
|
10
|
+
scope :bits_from, -> (value) { where(bits: value..) }
|
11
|
+
scope :bits_to, -> (value) { where(bits: ..value) }
|
12
|
+
|
13
|
+
scope :ip_address_starts_with, -> (string) { where("ip_address LIKE ?", sanitize_sql_like(string) + "%") }
|
14
|
+
scope :request_path_starts_with, -> (string) { where("request_path LIKE ?", sanitize_sql_like(string) + "%") }
|
15
|
+
|
16
|
+
def self.filter_by(params)
|
17
|
+
scope = all
|
18
|
+
scope = scope.request_path_starts_with(params[:request_path_starts_with]) if params[:request_path_starts_with].present?
|
19
|
+
scope = scope.ip_address_starts_with(params[:ip_address_starts_with]) if params[:ip_address_starts_with].present?
|
20
|
+
scope = scope.created_from(params[:created_from]) if params[:created_from].present?
|
21
|
+
scope = scope.created_to(params[:created_to]) if params[:created_to].present?
|
22
|
+
scope = scope.bits_from(params[:bits_from]) if params[:bits_from].present?
|
23
|
+
scope = scope.bits_to(params[:bits_to]) if params[:bits_to].present?
|
24
|
+
scope
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.spend(string, resource, bits, date, options = {})
|
28
|
+
return false unless stamp = parse(string)
|
29
|
+
stamp.attributes = options
|
30
|
+
stamp.verify(resource, bits, date) && stamp.save
|
31
|
+
rescue ActiveRecord::RecordNotUnique
|
32
|
+
false
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.parse(string)
|
36
|
+
return unless string
|
37
|
+
args = string.split(":")
|
38
|
+
return if args.size != 7
|
39
|
+
new(version: args[0], bits: args[1], date: Date.strptime(args[2], ActiveHashcash.date_format), resource: args[3], ext: args[4], rand: args[5], counter: args[6])
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.mint(resource, attributes = {})
|
43
|
+
new({
|
44
|
+
version: 1,
|
45
|
+
bits: ActiveHashcash.bits,
|
46
|
+
date: Date.today.strftime(ActiveHashcash.date_format),
|
47
|
+
resource: resource,
|
48
|
+
rand: SecureRandom.alphanumeric(16),
|
49
|
+
counter: 0,
|
50
|
+
}.merge(attributes)).work
|
51
|
+
end
|
52
|
+
|
53
|
+
def work
|
54
|
+
counter.next! until authentic?
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def authentic?
|
59
|
+
Digest::SHA1.hexdigest(to_s).hex >> (160-bits) == 0
|
60
|
+
end
|
61
|
+
|
62
|
+
def verify(resource, bits, date)
|
63
|
+
self.resource == resource && self.bits >= bits && self.date >= date && !self.date.future? && authentic?
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_s
|
67
|
+
[version.to_i, bits, date.strftime("%y%m%d"), resource, ext, rand, counter].join(":")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<details>
|
2
|
+
<summary>Filters</summary>
|
3
|
+
<%= render "active_hashcash/stamps/filters" %>
|
4
|
+
</details>
|
5
|
+
|
6
|
+
<table>
|
7
|
+
<tr>
|
8
|
+
<th><%= ActiveHashcash::Stamp.human_attribute_name(:ip_address) %></th>
|
9
|
+
<th><%= ActiveHashcash::Stamp.model_name.human.pluralize %></th>
|
10
|
+
</tr>
|
11
|
+
<% for (address, count) in @addresses %>
|
12
|
+
<tr>
|
13
|
+
<td><%= link_to address, stamps_path(ip_address_starts_with: address) %></td>
|
14
|
+
<td><%= number_with_delimiter count %></td>
|
15
|
+
</tr>
|
16
|
+
<% end %>
|
17
|
+
</table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<svg viewBox="0 0 50.4 50.4" xmlns="http://www.w3.org/2000/svg"><g fill="#08773c"><path d="m25.2 0c-13.9 0-25.2 11.3-25.2 25.2s11.3 25.2 25.2 25.2 25.2-11.3 25.2-25.2-11.31-25.2-25.2-25.2zm0 46.4c-11.69 0-21.2-9.51-21.2-21.2s9.51-21.2 21.2-21.2 21.2 9.51 21.2 21.2-9.51 21.2-21.2 21.2z"/><path d="m33 12.04h-4v7.58h-7.89v-7.58h-4v7.58h-4.98v4h4.98v2.69h-4.98v4h4.98v7.58h4v-7.58h7.89v7.58h4v-7.58h4.98v-4h-4.98v-2.69h4.98v-4h-4.98zm-4 14.27h-7.89v-2.69h7.89z"/></g></svg>
|