/** * @route notary/verify */ public function verify() { // Input validation if (empty($_POST['challenge'])) { \Airship\json_response(['status' => 'error', 'message' => 'Expected a challenge=something HTTP POST parameter.']); } if (!\is_string($_POST['challenge'])) { \Airship\json_response(['status' => 'error', 'message' => 'Challenge must be a string.']); } if (Binary::safeStrlen($_POST['challenge']) < 20) { \Airship\json_response(['status' => 'error', 'message' => 'Challenge is too short. Continuum should be generating a long random nonce.']); } try { list($update, $signature) = $this->chanUp->verifyUpdate($this->sk, $_POST['challenge']); \Airship\json_response(['status' => 'OK', 'response' => $update, 'signature' => $signature]); } catch (\Exception $ex) { \Airship\json_response(['status' => 'error', 'message' => $ex->getMessage()]); } }
/** * Convert a hexadecimal string into a binary string without cache-timing * leaks * * @param string $hex_string * @param bool $strictPadding * @return string (raw binary) * @throws \RangeException */ public static function decode(string $hexString, bool $strictPadding = false) : string { $hex_pos = 0; $bin = ''; $c_acc = 0; $hex_len = Binary::safeStrlen($hexString); $state = 0; if (($hex_len & 1) !== 0) { if ($strictPadding) { throw new \RangeException('Expected an even number of hexadecimal characters'); } else { $hexString = '0' . $hexString; ++$hex_len; } } $chunk = \unpack('C*', $hexString); while ($hex_pos < $hex_len) { ++$hex_pos; $c = $chunk[$hex_pos]; $c_num = $c ^ 48; $c_num0 = $c_num - 10 >> 8; $c_alpha = ($c & ~32) - 55; $c_alpha0 = ($c_alpha - 10 ^ $c_alpha - 16) >> 8; if (($c_num0 | $c_alpha0) === 0) { throw new \RangeException('hexEncode() only expects hexadecimal characters'); } $c_val = $c_num0 & $c_num | $c_alpha & $c_alpha0; if ($state === 0) { $c_acc = $c_val * 16; } else { $bin .= \pack('C', $c_acc | $c_val); } $state ^= 1; } return $bin; }
/** * Is this URL a Tor Hidden Service? * * @param string $url * @return bool */ function isOnionUrl(string $url) : bool { $host = \parse_url($url, \PHP_URL_HOST); if ($host !== null) { if (Binary::safeStrlen($host) < 7) { return false; } $suffix = Binary::safeSubstr($host, -6); return \strtolower($suffix) === '.onion'; } return false; }
/** * Generate, store, and return the index and token * * @param string $lockTo What URI endpoint this is valid for * @return string[] */ protected function generateToken(string $lockTo) : array { $index = Base64::encode(\random_bytes(18)); $token = Base64::encode(\random_bytes(33)); $this->session[$this->sessionIndex][$index] = ['created' => \intval(\date('YmdHis')), 'uri' => isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : $this->server['SCRIPT_NAME'], 'token' => $token]; if (\preg_match('#/$#', $lockTo)) { $lockTo = Binary::safeSubstr($lockTo, 0, Binary::safeStrlen($lockTo) - 1); } $this->session[$this->sessionIndex][$index]['lockTo'] = $lockTo; $this->recycleTokens(); return [$index, $token]; }
/** * decode from base64 into binary * * Base64 character set "./[A-Z][a-z][0-9]" * * @param string $src * @param bool $strictPadding * @return string|bool * @throws \RangeException */ public static function decode(string $src, bool $strictPadding = false) : string { // Remove padding $srcLen = Binary::safeStrlen($src); if ($srcLen === 0) { return ''; } if ($strictPadding) { if (($srcLen & 3) === 0) { if ($src[$srcLen - 1] === '=') { $srcLen--; if ($src[$srcLen - 1] === '=') { $srcLen--; } } } if (($srcLen & 3) === 1) { throw new \RangeException('Incorrect padding'); } if ($src[$srcLen - 1] === '=') { throw new \RangeException('Incorrect padding'); } } else { $src = \rtrim($src, '='); $srcLen = Binary::safeStrlen($src); } $err = 0; $dest = ''; // Main loop (no padding): for ($i = 0; $i + 4 <= $srcLen; $i += 4) { $chunk = \unpack('C*', Binary::safeSubstr($src, $i, 4)); $c0 = static::decode6Bits($chunk[1]); $c1 = static::decode6Bits($chunk[2]); $c2 = static::decode6Bits($chunk[3]); $c3 = static::decode6Bits($chunk[4]); $dest .= \pack('CCC', ($c0 << 2 | $c1 >> 4) & 0xff, ($c1 << 4 | $c2 >> 2) & 0xff, ($c2 << 6 | $c3) & 0xff); $err |= ($c0 | $c1 | $c2 | $c3) >> 8; } // The last chunk, which may have padding: if ($i < $srcLen) { $chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i)); $c0 = static::decode6Bits($chunk[1]); if ($i + 2 < $srcLen) { $c1 = static::decode6Bits($chunk[2]); $c2 = static::decode6Bits($chunk[3]); $dest .= \pack('CC', ($c0 << 2 | $c1 >> 4) & 0xff, ($c1 << 4 | $c2 >> 2) & 0xff); $err |= ($c0 | $c1 | $c2) >> 8; } elseif ($i + 1 < $srcLen) { $c1 = static::decode6Bits($chunk[2]); $dest .= \pack('C', ($c0 << 2 | $c1 >> 4) & 0xff); $err |= ($c0 | $c1) >> 8; } elseif ($i < $srcLen && $strictPadding) { $err |= 1; } } if ($err !== 0) { throw new \RangeException('Base64::decode() only expects characters in the correct base64 alphabet'); } return $dest; }
/** * Base32 Decoding * * @param string $src * @param bool $upper * @return string */ protected static function doEncode(string $src, bool $upper = false) : string { // We do this to reduce code duplication: $method = $upper ? 'encode5BitsUpper' : 'encode5Bits'; $dest = ''; $srcLen = Binary::safeStrlen($src); // Main loop (no padding): for ($i = 0; $i + 5 <= $srcLen; $i += 5) { $chunk = \unpack('C*', Binary::safeSubstr($src, $i, 5)); $b0 = $chunk[1]; $b1 = $chunk[2]; $b2 = $chunk[3]; $b3 = $chunk[4]; $b4 = $chunk[5]; $dest .= static::$method($b0 >> 3 & 31) . static::$method(($b0 << 2 | $b1 >> 6) & 31) . static::$method($b1 >> 1 & 31) . static::$method(($b1 << 4 | $b2 >> 4) & 31) . static::$method(($b2 << 1 | $b3 >> 7) & 31) . static::$method($b3 >> 2 & 31) . static::$method(($b3 << 3 | $b4 >> 5) & 31) . static::$method($b4 & 31); } // The last chunk, which may have padding: if ($i < $srcLen) { $chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i)); $b0 = $chunk[1]; if ($i + 3 < $srcLen) { $b1 = $chunk[2]; $b2 = $chunk[3]; $b3 = $chunk[4]; $dest .= static::$method($b0 >> 3 & 31) . static::$method(($b0 << 2 | $b1 >> 6) & 31) . static::$method($b1 >> 1 & 31) . static::$method(($b1 << 4 | $b2 >> 4) & 31) . static::$method(($b2 << 1 | $b3 >> 7) & 31) . static::$method($b3 >> 2 & 31) . static::$method($b3 << 3 & 31) . '='; } elseif ($i + 2 < $srcLen) { $b1 = $chunk[2]; $b2 = $chunk[3]; $dest .= static::$method($b0 >> 3 & 31) . static::$method(($b0 << 2 | $b1 >> 6) & 31) . static::$method($b1 >> 1 & 31) . static::$method(($b1 << 4 | $b2 >> 4) & 31) . static::$method($b2 << 1 & 31) . '==='; } elseif ($i + 1 < $srcLen) { $b1 = $chunk[2]; $dest .= static::$method($b0 >> 3 & 31) . static::$method(($b0 << 2 | $b1 >> 6) & 31) . static::$method($b1 >> 1 & 31) . static::$method($b1 << 4 & 31) . '===='; } else { $dest .= static::$method($b0 >> 3 & 31) . static::$method($b0 << 2 & 31) . '======'; } } return $dest; }
/** * Replace the existing long-term authentication cookie * * @param string $token * @param int $userId * @return mixed */ public function rotateToken(string $token, int $userId = 0) { try { $decoded = Base64::decode($token); } catch (\RangeException $ex) { return false; } if ($decoded === false) { return false; } elseif (Binary::safeStrlen($decoded) !== self::LONG_TERM_AUTH_BYTES) { return false; } $sel = Binary::safeSubstr($decoded, 0, self::SELECTOR_BYTES); \Sodium\memzero($decoded); // Delete the old token $this->db->delete($this->tableConfig['table']['longterm'], [$this->tableConfig['fields']['longterm']['selector'] => Base64::encode($sel)]); // Let's get a new token return $this->createAuthToken($userId); }