Class: Shugoi::Pow

Inherits:
Object
  • Object
show all
Defined in:
lib/shugoi/pow.rb

Overview

Proof-of-work anti-curl + cookies HMAC — parité avec core.ts (module Node) :

salt = HMAC(secret, ts)
proof = "ts:nonce" où SHA256(salt:nonce) a >= POW_DIFFICULTY bits à zéro en tête.
__sg_ok        : ts:HMAC(secret, "sg_ok:ts")         — 30 jours, saute le pre-flight PoW
__sg_authorized: ts:HMAC(secret, "sg_authorized:ts") — 120 s, protège les assets /assets/*

Constant Summary collapse

POW_OK_TTL_MS =
30 * 24 * 3600 * 1000
AUTHORIZED_TTL_MS =
120_000

Instance Method Summary collapse

Constructor Details

#initialize(signing_secret, difficulty = 14, ttl_ms = 60_000) ⇒ Pow

Returns a new instance of Pow.



13
14
15
16
17
# File 'lib/shugoi/pow.rb', line 13

def initialize(signing_secret, difficulty = 14, ttl_ms = 60_000)
  @secret = signing_secret.to_s
  @difficulty = difficulty
  @ttl_ms = ttl_ms
end

Instance Method Details

#challengeObject

Génère le challenge à injecter (window.__sg_pow).



20
21
22
23
# File 'lib/shugoi/pow.rb', line 20

def challenge
  ts = Utils.now_sec
  { ts: ts, salt: salt(ts), difficulty: @difficulty }
end


85
86
87
88
# File 'lib/shugoi/pow.rb', line 85

def sg_authorized_cookie
  secure = production? ? "; Secure" : ""
  "__sg_authorized=#{sg_authorized_value}; Path=/; HttpOnly; SameSite=Strict; Max-Age=120#{secure}"
end

#sg_authorized_valid?(cookie_val) ⇒ Boolean

Returns:

  • (Boolean)


72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/shugoi/pow.rb', line 72

def sg_authorized_valid?(cookie_val)
  return false if @secret.empty? || cookie_val.to_s.empty?
  ts_str, sig = cookie_val.to_s.split(":", 2)
  return false if ts_str.nil? || sig.nil?

  ts = ts_str.to_i
  return false if ts.zero?
  return false if Utils.now_ms - ts * 1000 > AUTHORIZED_TTL_MS
  return false if ts * 1000 > Utils.now_ms + 60_000

  Utils.secure_equals(sig, Utils.hmac_hex(@secret, "sg_authorized:#{ts_str}"))
end

#sg_authorized_valueObject

Cookie __sg_authorized posé par handleRender après un render réussi (grant valide).



67
68
69
70
# File 'lib/shugoi/pow.rb', line 67

def sg_authorized_value
  ts = Utils.now_sec
  "#{ts}:#{Utils.hmac_hex(@secret, "sg_authorized:#{ts}")}"
end

String Set-Cookie pour __sg_ok (posé par le middleware après une preuve valide).

Returns:

  • (String, nil)

    nil si la preuve n'est pas valide



60
61
62
63
64
# File 'lib/shugoi/pow.rb', line 60

def sg_ok_cookie(proof)
  return nil unless valid?(proof)
  secure = production? ? "; Secure" : ""
  "__sg_ok=#{sg_ok_value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=#{POW_OK_TTL_MS / 1000}#{secure}"
end

#sg_ok_valid?(cookie_val) ⇒ Boolean

Returns:

  • (Boolean)


45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/shugoi/pow.rb', line 45

def sg_ok_valid?(cookie_val)
  return false if @secret.empty? || cookie_val.to_s.empty?
  ts_str, sig = cookie_val.to_s.split(":", 2)
  return false if ts_str.nil? || sig.nil?

  ts = ts_str.to_i
  return false if ts.zero?
  return false if Utils.now_ms - ts * 1000 > POW_OK_TTL_MS
  return false if ts * 1000 > Utils.now_ms + 60_000

  Utils.secure_equals(sig, Utils.hmac_hex(@secret, "sg_ok:#{ts_str}"))
end

#sg_ok_valueObject

Valeur du cookie __sg_ok (HMAC serveur, 30 j).



40
41
42
43
# File 'lib/shugoi/pow.rb', line 40

def sg_ok_value
  ts = Utils.now_sec
  "#{ts}:#{Utils.hmac_hex(@secret, "sg_ok:#{ts}")}"
end

#valid?(proof) ⇒ Boolean

Vérifie un proof "ts:nonce" (fenêtre @ttl_ms, comptage de bits CORRIGÉ).

Returns:

  • (Boolean)


26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/shugoi/pow.rb', line 26

def valid?(proof)
  return false if proof.to_s.empty? || @secret.empty?
  ts_str, solution = proof.to_s.split(":", 2)
  return false if ts_str.nil? || solution.nil?

  ts = ts_str.to_i
  return false if ts.zero?
  return false if (Utils.now_ms - ts * 1000).abs > @ttl_ms

  digest = Utils.sha256_hex("#{salt(ts_str)}:#{solution}")
  Utils.leading_zero_bits(digest) >= @difficulty
end