@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class PhabricatorAuthCSRFEngine extends Phobject {
4
5 private $salt;
6 private $secret;
7
8 public function setSalt($salt) {
9 $this->salt = $salt;
10 return $this;
11 }
12
13 public function getSalt() {
14 return $this->salt;
15 }
16
17 public function setSecret(PhutilOpaqueEnvelope $secret) {
18 $this->secret = $secret;
19 return $this;
20 }
21
22 public function getSecret() {
23 return $this->secret;
24 }
25
26 public function newSalt() {
27 $salt_length = $this->getSaltLength();
28 return Filesystem::readRandomCharacters($salt_length);
29 }
30
31 public function newToken() {
32 $salt = $this->getSalt();
33
34 if (!$salt) {
35 throw new PhutilInvalidStateException('setSalt');
36 }
37
38 $token = $this->newRawToken($salt);
39 $prefix = $this->getBREACHPrefix();
40
41 return sprintf('%s%s%s', $prefix, $salt, $token);
42 }
43
44 public function isValidToken($token) {
45 $salt_length = $this->getSaltLength();
46
47 // We expect a BREACH-mitigating token. See T3684.
48 $breach_prefix = $this->getBREACHPrefix();
49 $breach_prelen = strlen($breach_prefix);
50 if (
51 $token === null ||
52 strncmp($token, $breach_prefix, $breach_prelen) !== 0
53 ) {
54 return false;
55 }
56
57 $salt = substr($token, $breach_prelen, $salt_length);
58 $token = substr($token, $breach_prelen + $salt_length);
59
60 foreach ($this->getWindowOffsets() as $offset) {
61 $expect_token = $this->newRawToken($salt, $offset);
62 if (phutil_hashes_are_identical($expect_token, $token)) {
63 return true;
64 }
65 }
66
67 return false;
68 }
69
70 private function newRawToken($salt, $offset = 0) {
71 $now = PhabricatorTime::getNow();
72 $cycle_frequency = $this->getCycleFrequency();
73
74 $time_block = (int)floor($now / $cycle_frequency);
75 $time_block = $time_block + $offset;
76
77 $secret = $this->getSecret();
78 if (!$secret) {
79 throw new PhutilInvalidStateException('setSecret');
80 }
81 $secret = $secret->openEnvelope();
82
83 $hash = PhabricatorHash::digestWithNamedKey(
84 $secret.$time_block.$salt,
85 'csrf');
86
87 return substr($hash, 0, $this->getTokenLength());
88 }
89
90 private function getBREACHPrefix() {
91 return 'B@';
92 }
93
94 private function getSaltLength() {
95 return 8;
96 }
97
98 private function getTokenLength() {
99 return 16;
100 }
101
102 private function getCycleFrequency() {
103 return phutil_units('1 hour in seconds');
104 }
105
106 private function getWindowOffsets() {
107 // We accept some tokens from the recent past and near future. Users may
108 // have older tokens if they close their laptop and open it up again
109 // later. Users may have newer tokens if there are multiple web hosts with
110 // a bit of clock skew.
111
112 // Javascript on the client tries to keep CSRF tokens up to date, but
113 // it may fail, and it doesn't run if the user closes their laptop.
114
115 // The window during which our tokens remain valid is generally more
116 // conservative than other platforms. For example, Rails uses "session
117 // duration" and Django uses "forever".
118
119 return range(-6, 1);
120 }
121
122}