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 + ':' + nonce)   (nonce 64 bits ALEATOIRE par challenge)
proof = "ts:nonce:solution" où SHA256(salt:solution) a >= POW_DIFFICULTY bits à zéro.
__sg_ok        : ts:ipBucket:uaFp:HMAC(secret, "sg_ok:ts:ipBucket:uaFp") — 30 jours,
               lié au bucket IP + empreinte UA (non rejouable depuis une autre IP),
               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.



17
18
19
20
21
# File 'lib/shugoi/pow.rb', line 17

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).



24
25
26
27
# File 'lib/shugoi/pow.rb', line 24

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


94
95
96
97
# File 'lib/shugoi/pow.rb', line 94

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)


81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/shugoi/pow.rb', line 81

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).



76
77
78
79
# File 'lib/shugoi/pow.rb', line 76

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



69
70
71
72
73
# File 'lib/shugoi/pow.rb', line 69

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

#sg_ok_valid?(cookie_val, ip = "", ua = "") ⇒ Boolean

Returns:

  • (Boolean)


52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/shugoi/pow.rb', line 52

def sg_ok_valid?(cookie_val, ip = "", ua = "")
  return false if @secret.empty? || cookie_val.to_s.empty?
  ts_str, bucket, fp, sig = cookie_val.to_s.split(":", 4)
  return false if ts_str.nil? || bucket.nil? || fp.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
  # Lier au bucket IP + UA courants : un cookie d'une autre IP/UA → invalide.
  return false unless bucket == ip_bucket(ip) && fp == ua_fp(ua)

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

#sg_ok_value(ip = "", ua = "") ⇒ Object

Valeur du cookie __sg_ok (HMAC serveur, 30 j) — lié au bucket IP + empreinte UA.



45
46
47
48
49
50
# File 'lib/shugoi/pow.rb', line 45

def sg_ok_value(ip = "", ua = "")
  ts = Utils.now_sec
  bucket = ip_bucket(ip)
  fp = ua_fp(ua)
  "#{ts}:#{bucket}:#{fp}:#{Utils.hmac_hex(@secret, "sg_ok:#{ts}:#{bucket}:#{fp}")}"
end

#valid?(proof) ⇒ Boolean

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

Returns:

  • (Boolean)


30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/shugoi/pow.rb', line 30

def valid?(proof)
  return false if proof.to_s.empty? || @secret.empty?
  ts_str, nonce, solution = proof.to_s.split(":", 3)
  return false if ts_str.nil? || nonce.nil? || solution.nil?
  return false unless nonce.match?(/\A[0-9a-f]{16}\z/)

  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, nonce)}:#{solution}")
  Utils.leading_zero_bits(digest) >= @difficulty
end