auth.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. <?php declare(strict_types=1);
  2. const USERNAME_REGEX = '^.{1,1024}$';
  3. const PASSWORD_REGEX = '^(?=.*[\p{Ll}])(?=.*[\p{Lu}])(?=.*[\p{N}]).{8,1024}|.{10,1024}$';
  4. const PLACEHOLDER_USERNAME = 'lain';
  5. const PLACEHOLDER_PASSWORD = '••••••••••••••••••••••••';
  6. // Password storage security
  7. const ALGO_PASSWORD = PASSWORD_ARGON2ID;
  8. const OPTIONS_PASSWORD = [
  9. 'memory_cost' => 8192,
  10. 'time_cost' => 2,
  11. 'threads' => 64,
  12. ];
  13. function checkUsernameFormat(string $username): void {
  14. if (preg_match('/' . USERNAME_REGEX . '/Du', $username) !== 1)
  15. output(403, 'Username malformed.');
  16. }
  17. function checkPasswordFormat(string $password): void {
  18. if (preg_match('/' . PASSWORD_REGEX . '/Du', $password) !== 1)
  19. output(403, 'Password malformed.');
  20. }
  21. function hashUsername(string $username): string {
  22. return base64_encode(sodium_crypto_pwhash(32, $username, hex2bin(query('select', 'params', ['name' => 'username_salt'], ['value'])[0]), 2**10, 2**14, SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13));
  23. }
  24. function hashPassword(string $password): string {
  25. return password_hash($password, ALGO_PASSWORD, OPTIONS_PASSWORD);
  26. }
  27. function usernameExists(string $username): bool {
  28. return isset(query('select', 'users', ['username' => $username], ['id'])[0]);
  29. }
  30. function checkPassword(string $id, string $password): bool {
  31. return password_verify($password, query('select', 'users', ['id' => $id], ['password'])[0]);
  32. }
  33. function outdatedPasswordHash(string $id): bool {
  34. return password_needs_rehash(query('select', 'users', ['id' => $id], ['password'])[0], ALGO_PASSWORD, OPTIONS_PASSWORD);
  35. }
  36. function changePassword(string $id, string $password): void {
  37. DB->prepare('UPDATE users SET password = :password WHERE id = :id')
  38. ->execute([':password' => hashPassword($password), ':id' => $id]);
  39. }
  40. function stopSession(): void {
  41. if (session_status() === PHP_SESSION_ACTIVE)
  42. session_destroy();
  43. }
  44. function logout(): never {
  45. stopSession();
  46. header('Clear-Site-Data: "*"');
  47. redir();
  48. }
  49. function setupDisplayUsername(string $display_username): void {
  50. $nonce = random_bytes(24);
  51. $key = sodium_crypto_aead_xchacha20poly1305_ietf_keygen();
  52. $cyphertext = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt(
  53. htmlspecialchars($display_username),
  54. '',
  55. $nonce,
  56. $key
  57. );
  58. $_SESSION['display-username-nonce'] = $nonce;
  59. setcookie(
  60. 'display-username-decryption-key',
  61. base64_encode($key),
  62. [
  63. 'expires' => time() + 432000,
  64. 'path' => CONF['common']['prefix'] . '/',
  65. 'secure' => true,
  66. 'httponly' => true,
  67. 'samesite' => 'Strict'
  68. ]
  69. );
  70. $_SESSION['display-username-cyphertext'] = $cyphertext;
  71. }
  72. function authDeleteUser(string $user_id): void {
  73. $user_services = explode(',', query('select', 'users', ['id' => $user_id], ['services'])[0]);
  74. foreach (SERVICES_USER as $service)
  75. if (in_array($service, $user_services, true) AND CONF['common']['services'][$service] !== 'enabled')
  76. output(503, sprintf(_('Your account can\'t be deleted because the %s service is currently unavailable.'), '<em>' . PAGES[$service]['index']['title'] . '</em>'));
  77. if (in_array('reg', $user_services, true))
  78. foreach (query('select', 'registry', ['username' => $user_id], ['domain']) as $domain)
  79. regDeleteDomain($domain, $user_id);
  80. if (in_array('ns', $user_services, true))
  81. foreach (query('select', 'zones', ['username' => $user_id], ['zone']) as $zone)
  82. nsDeleteZone($zone, $user_id);
  83. if (in_array('ht', $user_services, true)) {
  84. foreach (query('select', 'sites', ['username' => $user_id]) as $site)
  85. htDeleteSite($site['address'], $site['type'], $user_id);
  86. exescape([
  87. CONF['ht']['sudo_path'],
  88. '-u',
  89. CONF['ht']['tor_user'],
  90. '--',
  91. CONF['ht']['rm_path'],
  92. '-r',
  93. '--',
  94. CONF['ht']['tor_keys_path'] . '/' . $user_id,
  95. ], result_code: $code);
  96. if ($code !== 0)
  97. output(500, 'Can\'t remove Tor keys directory.');
  98. removeDirectory(CONF['ht']['tor_config_path'] . '/' . $user_id);
  99. exescape([
  100. CONF['ht']['sudo_path'],
  101. '-u',
  102. CONF['ht']['sftpgo_user'],
  103. '--',
  104. CONF['ht']['rm_path'],
  105. '-r',
  106. '--',
  107. CONF['ht']['ht_path'] . '/fs/' . $user_id
  108. ], result_code: $code);
  109. if ($code !== 0)
  110. output(500, 'Can\'t remove user\'s directory.');
  111. query('delete', 'ssh-keys', ['username' => $user_id]);
  112. }
  113. query('delete', 'users', ['id' => $user_id]);
  114. }
  115. function rateLimit(): void {
  116. if ((PAGE_METADATA['tokens_account_cost'] ?? 0) > 0)
  117. rateLimitAccount(PAGE_METADATA['tokens_account_cost']);
  118. if ((PAGE_METADATA['tokens_instance_cost'] ?? 0) > 0)
  119. rateLimitInstance(PAGE_METADATA['tokens_instance_cost']);
  120. }
  121. const MAX_ACCOUNT_TOKENS = 86400;
  122. function rateLimitAccount(int $requestedTokens): int {
  123. // Get
  124. $userData = query('select', 'users', ['id' => $_SESSION['id']]);
  125. $tokens = $userData[0]['bucket_tokens'];
  126. $bucketLastUpdate = $userData[0]['bucket_last_update'];
  127. // Compute
  128. $tokens = min(MAX_ACCOUNT_TOKENS, $tokens + (time() - $bucketLastUpdate));
  129. if ($requestedTokens > 0) {
  130. if ($requestedTokens > $tokens)
  131. output(453, _('Account rate limit reached, try again later.'));
  132. $tokens -= $requestedTokens;
  133. // Update
  134. DB->prepare('UPDATE users SET bucket_tokens = :bucket_tokens, bucket_last_update = :bucket_last_update WHERE id = :id')
  135. ->execute([
  136. ':bucket_tokens' => $tokens,
  137. ':bucket_last_update' => time(),
  138. ':id' => $_SESSION['id']
  139. ]);
  140. }
  141. return $tokens;
  142. }
  143. function rateLimitInstance(int $requestedTokens): void {
  144. // Get
  145. $tokens = query('select', 'params', ['name' => 'instance_bucket_tokens'], ['value'])[0];
  146. $bucketLastUpdate = query('select', 'params', ['name' => 'instance_bucket_last_update'], ['value'])[0];
  147. // Compute
  148. $tokens = min(86400, $tokens + (time() - $bucketLastUpdate));
  149. if ($requestedTokens > $tokens)
  150. output(453, _('Global rate limit reached, try again later.'));
  151. $tokens -= $requestedTokens;
  152. // Update
  153. DB->prepare("UPDATE params SET value = :bucket_tokens WHERE name = 'instance_bucket_tokens';")
  154. ->execute([':bucket_tokens' => $tokens]);
  155. DB->prepare("UPDATE params SET value = :bucket_last_update WHERE name = 'instance_bucket_last_update';")
  156. ->execute([':bucket_last_update' => time()]);
  157. }