active_hashcash 0.2.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
![Active Hashcash GIF preview](demo.gif)
|
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
|
+
![ActiveHashcash dashboard](active_hashcash_dashboard.png "ActiveHashcash dashboard")
|
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>
|