active_hashcash 0.4.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +19 -7
- data/app/assets/javascripts/hashcash.js +215 -173
- data/app/models/active_hashcash/stamp.rb +7 -2
- data/lib/active_hashcash/version.rb +1 -1
- data/lib/active_hashcash.rb +8 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ef5393e959b8a94792f35bc043c87542568398d3c9444a8e238ba6eafdbb3522
|
|
4
|
+
data.tar.gz: 74a51e94325581bf35113f81ccaccd696079bc43cfedc7f734ae5889ab0bb32f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5d0da77fac447177abebbf04a585927f42d7a100159bf9ab8df1305f1f88b9a394738c03f5a828177dea71f745a941b9bff7a8625c4cc8c5744fc735ef017cc2
|
|
7
|
+
data.tar.gz: 6845cb211b39fa5dfe0f0393df05ae7cd6e5b54f5e8f3e27e26a206f9853b026a7a557e0c5dbbf97e2eb1bc5468b4ef418cec31a9e20c98153b8027c13d30947
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog of ActiveHashcash
|
|
2
2
|
|
|
3
|
+
## 0.5.0 (2026-08-08)
|
|
4
|
+
|
|
5
|
+
- Fix stamp date mismatch by using server date instead of client
|
|
6
|
+
- Replace SHA-1 with SHA-256 for proof-of-work stamps
|
|
7
|
+
- Mine stamps in a Web Worker using pure JS SHA-256 (keeps the main thread unblocked)
|
|
8
|
+
- Support SHA-1 fallback on the backend for backward compatibility (via the `ext` stamp field)
|
|
9
|
+
|
|
3
10
|
## 0.4.0 (2025-05-15)
|
|
4
11
|
|
|
5
12
|
- Prevent from password managers to submit the form before the stamp has been computed
|
data/README.md
CHANGED
|
@@ -179,16 +179,28 @@ You can change the minimum complexity with `ActiveHashcash.bits = 20`.
|
|
|
179
179
|
Since version 0.3.0, the complexity increases with the number of stamps spent during le last 24H from the same IP address.
|
|
180
180
|
Thus it becomes very efficient to slow down brute force attacks.
|
|
181
181
|
|
|
182
|
+
## Testing
|
|
183
|
+
|
|
184
|
+
Browser tests submit the real form, so they have to compute a real stamp. At the default 16 bits this adds noticeable time to every submission and slows down your suite. Drop the complexity in the test environment so it finishes almost instantly:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
# spec/rails_helper.rb (RSpec) or test/test_helper.rb (Minitest)
|
|
188
|
+
ActiveHashcash.bits = 2
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
In controller tests, provide the hashcash this way:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
post(url, params: {hashcash: ActiveHashcash::Stamp.mint(host).to_s})
|
|
195
|
+
```
|
|
196
|
+
|
|
182
197
|
## Limitations
|
|
183
198
|
|
|
184
|
-
The JavaScript implementation is
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
Another idea would be to compile the work algorithm in wasm.
|
|
199
|
+
The JavaScript implementation is slower than the official C version.
|
|
200
|
+
It uses a pure JS SHA-256 implementation running inside a Web Worker, which keeps the main thread responsive while mining.
|
|
201
|
+
A synchronous tight loop avoids the per-call async overhead of `crypto.subtle.digest()`, making it the fastest browser-side approach across Chrome and Safari.
|
|
188
202
|
|
|
189
|
-
|
|
190
|
-
Maybe you have good JS skills to optimize it?
|
|
191
|
-
Any help would be appreciate to better fights bots and brute for attacks!
|
|
203
|
+
No `crypto.subtle` or secure context (HTTPS) is required, so it works in any environment including plain HTTP during development.
|
|
192
204
|
|
|
193
205
|
## Contributing
|
|
194
206
|
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
// http://www.hashcash.org/docs/hashcash.html
|
|
2
|
-
// <input type="
|
|
2
|
+
// <input type="hidden" name="hashcash" data-hashcash="{resource: 'site.example', bits: 16}"/>
|
|
3
3
|
Hashcash = function(input) {
|
|
4
4
|
options = JSON.parse(input.getAttribute("data-hashcash"))
|
|
5
5
|
Hashcash.disableParentForm(input, options)
|
|
6
6
|
input.dispatchEvent(new CustomEvent("hashcash:mint", {bubbles: true}))
|
|
7
7
|
|
|
8
8
|
Hashcash.mint(options.resource, options, function(stamp) {
|
|
9
|
+
// Guard against Turbo navigation: if the input is no longer in the DOM,
|
|
10
|
+
// the user has navigated away and we should silently discard the result.
|
|
11
|
+
if (!input.isConnected) return
|
|
12
|
+
|
|
9
13
|
input.value = stamp.toString()
|
|
10
14
|
Hashcash.enableParentForm(input, options)
|
|
11
15
|
input.dispatchEvent(new CustomEvent("hashcash:minted", {bubbles: true, detail: {stamp: stamp}}))
|
|
@@ -20,9 +24,39 @@ Hashcash.setup = function() {
|
|
|
20
24
|
var input = document.querySelector("input#hashcash")
|
|
21
25
|
input && new Hashcash(input)
|
|
22
26
|
} else
|
|
23
|
-
document.addEventListener("DOMContentLoaded", Hashcash.setup
|
|
27
|
+
document.addEventListener("DOMContentLoaded", Hashcash.setup)
|
|
24
28
|
}
|
|
25
29
|
|
|
30
|
+
// Terminate the active worker and clean up its Blob URL.
|
|
31
|
+
Hashcash.cleanup = function() {
|
|
32
|
+
if (Hashcash._worker) {
|
|
33
|
+
Hashcash._worker.terminate()
|
|
34
|
+
Hashcash._worker = null
|
|
35
|
+
}
|
|
36
|
+
if (Hashcash._workerUrl) {
|
|
37
|
+
URL.revokeObjectURL(Hashcash._workerUrl)
|
|
38
|
+
Hashcash._workerUrl = null
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Turbo Drive: terminate the worker when navigating away so it doesn't
|
|
43
|
+
// complete after the page has changed, and restore the form state for
|
|
44
|
+
// Turbo's page cache so the snapshot doesn't have disabled buttons.
|
|
45
|
+
document.addEventListener("turbo:before-visit", Hashcash.cleanup)
|
|
46
|
+
document.addEventListener("turbo:before-cache", function() {
|
|
47
|
+
Hashcash.cleanup()
|
|
48
|
+
var input = document.querySelector("input#hashcash")
|
|
49
|
+
if (input && input.form) {
|
|
50
|
+
input.value = ""
|
|
51
|
+
input.form.querySelectorAll("[type=submit]").forEach(function(submit) {
|
|
52
|
+
if (submit.originalValue) {
|
|
53
|
+
Hashcash.setSubmitText(submit, submit.originalValue)
|
|
54
|
+
}
|
|
55
|
+
submit.disabled = null
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
26
60
|
Hashcash.setSubmitText = function(submit, text) {
|
|
27
61
|
if (!text) {
|
|
28
62
|
return
|
|
@@ -57,62 +91,45 @@ Hashcash.prototype.preventFromAutoSubmitFromPasswordManagers = function(event) {
|
|
|
57
91
|
Hashcash.default = {
|
|
58
92
|
version: 1,
|
|
59
93
|
bits: 20,
|
|
60
|
-
extension: null,
|
|
61
94
|
}
|
|
62
95
|
|
|
63
96
|
Hashcash.mint = function(resource, options, callback) {
|
|
64
|
-
// Format date to YYMMDD
|
|
65
|
-
var date = new Date
|
|
66
|
-
var year = date.getFullYear().toString()
|
|
67
|
-
year = year.slice(year.length - 2, year.length)
|
|
68
|
-
var month = (date.getMonth() + 1).toString().padStart(2, "0")
|
|
69
|
-
var day = date.getDate().toString().padStart(2, "0")
|
|
70
|
-
|
|
71
97
|
var stamp = new Hashcash.Stamp(
|
|
72
98
|
options.version || Hashcash.default.version,
|
|
73
99
|
options.bits || Hashcash.default.bits,
|
|
74
|
-
options.date ||
|
|
100
|
+
options.date || Hashcash.formatToday(),
|
|
75
101
|
resource,
|
|
76
|
-
options.
|
|
77
|
-
options.rand || Math.random().toString(36).substr(2, 10),
|
|
102
|
+
options.rand || Math.random().toString(36).substr(2, 10)
|
|
78
103
|
)
|
|
79
104
|
return stamp.work(callback)
|
|
80
105
|
}
|
|
81
106
|
|
|
82
|
-
|
|
107
|
+
// Format date to YYMMDD
|
|
108
|
+
Hashcash.formatToday = function() {
|
|
109
|
+
var date = new Date
|
|
110
|
+
var year = date.getFullYear().toString()
|
|
111
|
+
year = year.slice(year.length - 2, year.length)
|
|
112
|
+
var month = (date.getMonth() + 1).toString().padStart(2, "0")
|
|
113
|
+
var day = date.getDate().toString().padStart(2, "0")
|
|
114
|
+
return year + month + day
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
Hashcash.Stamp = function(version, bits, date, resource, rand, counter) {
|
|
83
118
|
this.version = version
|
|
84
119
|
this.bits = bits
|
|
85
120
|
this.date = date
|
|
86
121
|
this.resource = resource
|
|
87
|
-
this.extension = extension
|
|
88
122
|
this.rand = rand
|
|
89
|
-
this.counter = counter
|
|
123
|
+
this.counter = counter || 0
|
|
90
124
|
}
|
|
91
125
|
|
|
92
126
|
Hashcash.Stamp.parse = function(string) {
|
|
93
127
|
var args = string.split(":")
|
|
94
|
-
return new Hashcash.Stamp(args[0], args[1], args[2], args[3], args[
|
|
128
|
+
return new Hashcash.Stamp(args[0], args[1], args[2], args[3], args[5], args[6])
|
|
95
129
|
}
|
|
96
130
|
|
|
97
131
|
Hashcash.Stamp.prototype.toString = function() {
|
|
98
|
-
return [this.version, this.bits, this.date, this.resource,
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Trigger the given callback when the problem is solved.
|
|
102
|
-
// In order to not freeze the page, setTimeout is called every 100ms to let some CPU to other tasks.
|
|
103
|
-
Hashcash.Stamp.prototype.work = function(callback) {
|
|
104
|
-
this.startClock()
|
|
105
|
-
var timer = performance.now()
|
|
106
|
-
while (!this.check())
|
|
107
|
-
if (this.counter++ && performance.now() - timer > 100)
|
|
108
|
-
return setTimeout(this.work.bind(this), 0, callback)
|
|
109
|
-
this.stopClock()
|
|
110
|
-
callback(this)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
Hashcash.Stamp.prototype.check = function() {
|
|
114
|
-
var array = Hashcash.sha1(this.toString())
|
|
115
|
-
return array[0] >> (160-this.bits) == 0
|
|
132
|
+
return [this.version, this.bits, this.date, this.resource, "sha256", this.rand, this.counter].join(":")
|
|
116
133
|
}
|
|
117
134
|
|
|
118
135
|
Hashcash.Stamp.prototype.startClock = function() {
|
|
@@ -126,151 +143,176 @@ Hashcash.Stamp.prototype.stopClock = function() {
|
|
|
126
143
|
console.debug("Hashcash " + this.toString() + " minted in " + duration + "ms (" + speed + " per seconds)")
|
|
127
144
|
}
|
|
128
145
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
var
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
E = D;
|
|
192
|
-
D = C;
|
|
193
|
-
C = rotate_left(B, 30);
|
|
194
|
-
B = A;
|
|
195
|
-
A = temp;
|
|
146
|
+
// Mine a valid stamp using a Web Worker with a pure JS SHA-256 implementation.
|
|
147
|
+
// The worker is inlined as a Blob URL so no extra file needs to be served.
|
|
148
|
+
// Using a Web Worker keeps the main thread completely unblocked while mining.
|
|
149
|
+
// A synchronous SHA-256 in a tight loop is faster than async crypto.subtle
|
|
150
|
+
// because it avoids the per-call Promise/microtask overhead entirely.
|
|
151
|
+
Hashcash.Stamp.prototype.work = function(callback) {
|
|
152
|
+
this.startClock()
|
|
153
|
+
var self = this
|
|
154
|
+
|
|
155
|
+
var workerCode = function() {
|
|
156
|
+
// SHA-256 round constants (FIPS 180-4 §4.2.2)
|
|
157
|
+
var K = new Uint32Array([
|
|
158
|
+
0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
|
|
159
|
+
0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
|
|
160
|
+
0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
|
|
161
|
+
0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
|
|
162
|
+
0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
|
|
163
|
+
0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
|
|
164
|
+
0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
|
|
165
|
+
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
// Compute SHA-256 over the first `len` bytes of `bytes`. Returns an array
|
|
169
|
+
// of 8 big-endian 32-bit integers (the 256-bit digest).
|
|
170
|
+
function sha256bytes(bytes, len) {
|
|
171
|
+
// Pad to a multiple of 64 bytes: append 0x80, then zeros, then 64-bit
|
|
172
|
+
// big-endian bit length (we only use the low 32 bits since stamp strings
|
|
173
|
+
// are always shorter than 512 MB).
|
|
174
|
+
var padded = new Uint8Array(((len + 9 + 63) & ~63));
|
|
175
|
+
for (var i = 0; i < len; i++) padded[i] = bytes[i];
|
|
176
|
+
padded[len] = 0x80;
|
|
177
|
+
var bitLen = len * 8;
|
|
178
|
+
var pLen = padded.length;
|
|
179
|
+
padded[pLen - 4] = (bitLen >>> 24) & 0xff;
|
|
180
|
+
padded[pLen - 3] = (bitLen >>> 16) & 0xff;
|
|
181
|
+
padded[pLen - 2] = (bitLen >>> 8) & 0xff;
|
|
182
|
+
padded[pLen - 1] = bitLen & 0xff;
|
|
183
|
+
|
|
184
|
+
// Initial hash values (FIPS 180-4 §5.3.3)
|
|
185
|
+
var H0 = 0x6a09e667, H1 = 0xbb67ae85, H2 = 0x3c6ef372, H3 = 0xa54ff53a;
|
|
186
|
+
var H4 = 0x510e527f, H5 = 0x9b05688c, H6 = 0x1f83d9ab, H7 = 0x5be0cd19;
|
|
187
|
+
var W = new Int32Array(64);
|
|
188
|
+
|
|
189
|
+
// Process each 512-bit (64-byte) block
|
|
190
|
+
for (var off = 0; off < pLen; off += 64) {
|
|
191
|
+
for (var t = 0; t < 16; t++) {
|
|
192
|
+
W[t] = (padded[off+t*4]<<24)|(padded[off+t*4+1]<<16)|(padded[off+t*4+2]<<8)|padded[off+t*4+3];
|
|
193
|
+
}
|
|
194
|
+
for (var t = 16; t < 64; t++) {
|
|
195
|
+
var w15 = W[t-15], w2 = W[t-2];
|
|
196
|
+
W[t] = (W[t-16] + (((w15>>>7)|(w15<<25))^((w15>>>18)|(w15<<14))^(w15>>>3)) + W[t-7] + (((w2>>>17)|(w2<<15))^((w2>>>19)|(w2<<13))^(w2>>>10))) | 0;
|
|
197
|
+
}
|
|
198
|
+
var a=H0,b=H1,c=H2,d=H3,e=H4,f=H5,g=H6,h=H7;
|
|
199
|
+
for (var t = 0; t < 64; t++) {
|
|
200
|
+
var t1 = (h + (((e>>>6)|(e<<26))^((e>>>11)|(e<<21))^((e>>>25)|(e<<7))) + ((e&f)^(~e&g)) + K[t] + W[t]) | 0;
|
|
201
|
+
var t2 = ((((a>>>2)|(a<<30))^((a>>>13)|(a<<19))^((a>>>22)|(a<<10))) + ((a&b)^(a&c)^(b&c))) | 0;
|
|
202
|
+
h=g; g=f; f=e; e=(d+t1)|0; d=c; c=b; b=a; a=(t1+t2)|0;
|
|
203
|
+
}
|
|
204
|
+
H0=(H0+a)|0; H1=(H1+b)|0; H2=(H2+c)|0; H3=(H3+d)|0;
|
|
205
|
+
H4=(H4+e)|0; H5=(H5+f)|0; H6=(H6+g)|0; H7=(H7+h)|0;
|
|
206
|
+
}
|
|
207
|
+
return [H0, H1, H2, H3, H4, H5, H6, H7];
|
|
196
208
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
209
|
+
|
|
210
|
+
// Encode a string as bytes into a pre-allocated buffer at `offset`.
|
|
211
|
+
// Handles ASCII and basic UTF-8 (BMP only).
|
|
212
|
+
function strToBytes(str, out, offset) {
|
|
213
|
+
for (var i = 0; i < str.length; i++) {
|
|
214
|
+
var c = str.charCodeAt(i);
|
|
215
|
+
if (c < 128) out[offset++] = c;
|
|
216
|
+
else if (c < 2048) { out[offset++] = (c>>6)|192; out[offset++] = (c&63)|128; }
|
|
217
|
+
else { out[offset++] = (c>>12)|224; out[offset++] = ((c>>6)&63)|128; out[offset++] = (c&63)|128; }
|
|
218
|
+
}
|
|
219
|
+
return offset;
|
|
204
220
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
221
|
+
|
|
222
|
+
// Write the decimal digits of `n` as ASCII bytes into `out` at `offset`.
|
|
223
|
+
// Avoids string allocation in the hot loop.
|
|
224
|
+
function numToBytes(n, out, offset) {
|
|
225
|
+
if (n === 0) { out[offset] = 48; return offset + 1; }
|
|
226
|
+
var digs = [];
|
|
227
|
+
while (n > 0) { digs.push(48 + (n % 10)); n = (n / 10) | 0; }
|
|
228
|
+
for (var i = digs.length - 1; i >= 0; i--) out[offset++] = digs[i];
|
|
229
|
+
return offset;
|
|
212
230
|
}
|
|
213
|
-
H0 = (H0 + A) & 0x0ffffffff;
|
|
214
|
-
H1 = (H1 + B) & 0x0ffffffff;
|
|
215
|
-
H2 = (H2 + C) & 0x0ffffffff;
|
|
216
|
-
H3 = (H3 + D) & 0x0ffffffff;
|
|
217
|
-
H4 = (H4 + E) & 0x0ffffffff;
|
|
218
|
-
}
|
|
219
|
-
return [H0, H1, H2, H3, H4]
|
|
220
|
-
}
|
|
221
231
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
232
|
+
self.addEventListener("message", function(e) {
|
|
233
|
+
var d = e.data;
|
|
234
|
+
var prefix = d.prefix + ":";
|
|
235
|
+
var bits = d.bits;
|
|
236
|
+
var counter = 0;
|
|
227
237
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
238
|
+
// Pre-compute how many leading zero bytes and remaining bits to check
|
|
239
|
+
var fullBytes = Math.floor(bits / 8);
|
|
240
|
+
var remBits = bits % 8;
|
|
241
|
+
var mask = remBits > 0 ? (0xFF << (8 - remBits)) & 0xFF : 0;
|
|
242
|
+
|
|
243
|
+
// Pre-encode the stamp prefix (everything before the counter) into a
|
|
244
|
+
// reusable byte buffer. Only the counter digits change per iteration.
|
|
245
|
+
var buf = new Uint8Array(prefix.length + 12);
|
|
246
|
+
var prefixLen = strToBytes(prefix, buf, 0);
|
|
247
|
+
|
|
248
|
+
// Yield every 65536 iterations so the worker stays terminable.
|
|
249
|
+
var YIELD = 65536;
|
|
250
|
+
|
|
251
|
+
function mine() {
|
|
252
|
+
var end = counter + YIELD;
|
|
253
|
+
while (counter < end) {
|
|
254
|
+
var len = numToBytes(counter, buf, prefixLen);
|
|
255
|
+
var H = sha256bytes(buf, len);
|
|
256
|
+
|
|
257
|
+
// Check leading zero bits by walking the digest words byte-by-byte.
|
|
258
|
+
var ok = true, byteIdx = 0;
|
|
259
|
+
check:
|
|
260
|
+
for (var w = 0; w < 8 && byteIdx <= fullBytes; w++) {
|
|
261
|
+
var word = H[w];
|
|
262
|
+
for (var s = 24; s >= 0; s -= 8) {
|
|
263
|
+
var bv = (word >>> s) & 0xFF;
|
|
264
|
+
if (byteIdx < fullBytes) {
|
|
265
|
+
if (bv !== 0) { ok = false; break check; }
|
|
266
|
+
} else if (byteIdx === fullBytes && remBits > 0) {
|
|
267
|
+
if ((bv & mask) !== 0) ok = false;
|
|
268
|
+
break check;
|
|
269
|
+
} else { break check; }
|
|
270
|
+
byteIdx++;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (ok) { self.postMessage({ found: true, counter: counter }); return; }
|
|
274
|
+
counter++;
|
|
275
|
+
}
|
|
276
|
+
setTimeout(mine, 0);
|
|
277
|
+
}
|
|
278
|
+
mine();
|
|
279
|
+
});
|
|
253
280
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
281
|
+
|
|
282
|
+
// Clean up any previous worker (e.g. Turbo restoring a cached page)
|
|
283
|
+
Hashcash.cleanup()
|
|
284
|
+
|
|
285
|
+
var blob = new Blob(
|
|
286
|
+
["(" + workerCode.toString() + ")()"],
|
|
287
|
+
{type: "application/javascript"}
|
|
288
|
+
)
|
|
289
|
+
var workerUrl = URL.createObjectURL(blob)
|
|
290
|
+
var worker = new Worker(workerUrl)
|
|
291
|
+
|
|
292
|
+
// Track the active worker so Hashcash.cleanup() can terminate it
|
|
293
|
+
Hashcash._worker = worker
|
|
294
|
+
Hashcash._workerUrl = workerUrl
|
|
295
|
+
|
|
296
|
+
worker.onmessage = function(e) {
|
|
297
|
+
if (e.data.found) {
|
|
298
|
+
Hashcash._worker = null
|
|
299
|
+
Hashcash._workerUrl = null
|
|
300
|
+
self.counter = e.data.counter
|
|
301
|
+
self.stopClock()
|
|
302
|
+
worker.terminate()
|
|
303
|
+
URL.revokeObjectURL(workerUrl)
|
|
304
|
+
callback(self)
|
|
271
305
|
}
|
|
272
306
|
}
|
|
273
|
-
|
|
274
|
-
|
|
307
|
+
|
|
308
|
+
// Build the prefix once and send it to the worker. The worker appends the
|
|
309
|
+
// counter on each iteration, avoiding repeated string building.
|
|
310
|
+
var prefix = [this.version, this.bits, this.date, this.resource, "sha256", this.rand].join(":")
|
|
311
|
+
|
|
312
|
+
worker.postMessage({
|
|
313
|
+
prefix: prefix,
|
|
314
|
+
bits: parseInt(this.bits, 10)
|
|
315
|
+
})
|
|
316
|
+
}
|
|
275
317
|
|
|
276
318
|
Hashcash.setup()
|
|
@@ -36,7 +36,7 @@ module ActiveHashcash
|
|
|
36
36
|
false
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
#
|
|
39
|
+
# Parse and instantiate a stamp from a string which respects the hashcash format:
|
|
40
40
|
#
|
|
41
41
|
# ver:bits:date:resource:[ext]:rand:counter
|
|
42
42
|
def self.parse(string)
|
|
@@ -51,6 +51,7 @@ module ActiveHashcash
|
|
|
51
51
|
bits: ActiveHashcash.bits,
|
|
52
52
|
date: Date.today.strftime(ActiveHashcash.date_format),
|
|
53
53
|
resource: resource,
|
|
54
|
+
ext: "sha256",
|
|
54
55
|
rand: SecureRandom.alphanumeric(16),
|
|
55
56
|
counter: 0,
|
|
56
57
|
}.merge(attributes)).work
|
|
@@ -62,7 +63,11 @@ module ActiveHashcash
|
|
|
62
63
|
end
|
|
63
64
|
|
|
64
65
|
def authentic?
|
|
65
|
-
|
|
66
|
+
if ext == "sha256"
|
|
67
|
+
Digest::SHA256.hexdigest(to_s).hex >> (256 - bits) == 0
|
|
68
|
+
else
|
|
69
|
+
Digest::SHA1.hexdigest(to_s).hex >> (160 - bits) == 0
|
|
70
|
+
end
|
|
66
71
|
end
|
|
67
72
|
|
|
68
73
|
def verify(resource, bits, date)
|
data/lib/active_hashcash.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "active_hashcash/version"
|
|
2
4
|
require "active_hashcash/engine"
|
|
3
5
|
|
|
@@ -118,7 +120,12 @@ module ActiveHashcash
|
|
|
118
120
|
# <% end %>
|
|
119
121
|
#
|
|
120
122
|
def hashcash_hidden_field_tag(name = :hashcash)
|
|
121
|
-
options = {
|
|
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
|
+
}
|
|
122
129
|
view_context.hidden_field_tag(name, "", "data-hashcash" => options.to_json)
|
|
123
130
|
end
|
|
124
131
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_hashcash
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alexis Bernard
|
|
@@ -96,7 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
96
96
|
- !ruby/object:Gem::Version
|
|
97
97
|
version: '0'
|
|
98
98
|
requirements: []
|
|
99
|
-
rubygems_version:
|
|
99
|
+
rubygems_version: 4.0.10
|
|
100
100
|
specification_version: 4
|
|
101
101
|
summary: Protect Rails applications against bots and brute force attacks without annoying
|
|
102
102
|
humans.
|