浏览代码

Internal ID, Argon2 for usernames, username changes

Miraty 2 年之前
父节点
当前提交
f15681999b

+ 21 - 19
db/schema.sql

@@ -1,41 +1,43 @@
 BEGIN TRANSACTION;
 CREATE TABLE IF NOT EXISTS "params" (
 	"name"  TEXT NOT NULL UNIQUE,
-	"value" TEXT NOT NULL
+	"value" TEXT NOT NULL,
+	PRIMARY KEY("name")
+);
+INSERT INTO "params"("name", "value") VALUES("instance_bucket_tokens", "0");
+INSERT INTO "params"("name", "value") VALUES("instance_bucket_last_update", "0");
+INSERT INTO "params"("name", "value") VALUES("username_salt", "00000000000000000000000000000000"); -- Should be unique and secret ; generate one using `openssl rand -hex 16` ; can't be changed without breaking current accounts login
+CREATE TABLE IF NOT EXISTS "users" (
+	"id"                 TEXT    NOT NULL UNIQUE,
+	"username"           TEXT    NOT NULL UNIQUE,
+	"password"           TEXT    NOT NULL,
+	"registration_date"  TEXT    NOT NULL,
+	"bucket_tokens"      INTEGER NOT NULL,
+	"bucket_last_update" INTEGER NOT NULL,
+	"type"               TEXT    NOT NULL,
+	PRIMARY KEY("id")
 );
 CREATE TABLE IF NOT EXISTS "registry" (
-	"id"           INTEGER NOT NULL UNIQUE,
 	"domain"       TEXT    NOT NULL UNIQUE,
 	"username"     TEXT    NOT NULL,
 	"last_renewal" TEXT    NOT NULL,
-	PRIMARY KEY("id" AUTOINCREMENT)
+	PRIMARY KEY("domain"),
+	FOREIGN KEY("username") REFERENCES "users"("id")
 );
 CREATE TABLE IF NOT EXISTS "zones" (
-	"id"       INTEGER NOT NULL UNIQUE,
 	"zone"     TEXT    NOT NULL UNIQUE,
 	"username" TEXT    NOT NULL,
-	PRIMARY KEY("id" AUTOINCREMENT)
-);
-CREATE TABLE IF NOT EXISTS "users" (
-	"id"                 INTEGER NOT NULL UNIQUE,
-	"username"           TEXT    NOT NULL UNIQUE,
-	"password"           TEXT    NOT NULL,
-	"registration_date"  TEXT    NOT NULL,
-	"bucket_tokens"      INTEGER NOT NULL,
-	"bucket_last_update" INTEGER NOT NULL,
-	"type"               TEXT    NOT NULL,
-	PRIMARY KEY("id" AUTOINCREMENT)
+	PRIMARY KEY("zone"),
+	FOREIGN KEY("username") REFERENCES "users"("id")
 );
 CREATE TABLE IF NOT EXISTS "sites" (
-	"id"            INTEGER NOT NULL UNIQUE,
 	"username"      TEXT    NOT NULL,
 	"site_dir"      TEXT    NOT NULL,
 	"domain"        TEXT    NOT NULL UNIQUE,
 	"domain_type"   TEXT    NOT NULL,
 	"protocol"      TEXT    NOT NULL,
 	"creation_date" TEXT    NOT NULL,
-	PRIMARY KEY("id" AUTOINCREMENT)
+	PRIMARY KEY("domain"),
+	FOREIGN KEY("username") REFERENCES "users"("id")
 );
-INSERT INTO "params"("name", "value") VALUES("instance_bucket_tokens", "0");
-INSERT INTO "params"("name", "value") VALUES("instance_bucket_last_update", "0");
 COMMIT;

+ 19 - 15
fn/auth.php

@@ -14,38 +14,42 @@ const OPTIONS_PASSWORD = [
 	'threads' => 64,
 ];
 
+function checkUsernameFormat($username) {
+	if (preg_match('/' . USERNAME_REGEX . '/Du', $username) !== 1)
+		output(403, 'Username malformed.');
+}
+
 function checkPasswordFormat($password) {
 	if (preg_match('/' . PASSWORD_REGEX . '/Du', $password) !== 1)
 		output(403, 'Password malformed.');
 }
 
-function checkUsernameFormat($username) {
-	if (preg_match('/' . USERNAME_REGEX . '/Du', $username) !== 1)
-		output(403, 'Username malformed.');
+function hashUsername($username) {
+	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));
 }
 
 function hashPassword($password) {
 	return password_hash($password, ALGO_PASSWORD, OPTIONS_PASSWORD);
 }
 
-function userExist($username) {
-	return isset(query('select', 'users', ['username' => $username], 'username')[0]);
+function usernameExists($username) {
+	return isset(query('select', 'users', ['username' => $username], 'id')[0]);
 }
 
-function checkPassword($username, $password) {
-	return password_verify($password, query('select', 'users', ['username' => $username], 'password')[0]);
+function checkPassword($id, $password) {
+	return password_verify($password, query('select', 'users', ['id' => $id], 'password')[0]);
 }
 
-function outdatedPasswordHash($username) {
-	return password_needs_rehash(query('select', 'users', ['username' => $username], 'password')[0], ALGO_PASSWORD, OPTIONS_PASSWORD);
+function outdatedPasswordHash($id) {
+	return password_needs_rehash(query('select', 'users', ['id' => $id], 'password')[0], ALGO_PASSWORD, OPTIONS_PASSWORD);
 }
 
-function changePassword($username, $password) {
+function changePassword($id, $password) {
 	$db = new PDO('sqlite:' . DB_PATH);
 
-	$stmt = $db->prepare('UPDATE users SET password = :password WHERE username = :username');
+	$stmt = $db->prepare('UPDATE users SET password = :password WHERE id = :id');
 
-	$stmt->bindValue(':username', $username);
+	$stmt->bindValue(':id', $id);
 	$stmt->bindValue(':password', hashPassword($password));
 
 	$stmt->execute();
@@ -61,7 +65,7 @@ function rateLimit() {
 
 function rateLimitAccount($requestedTokens) {
 	// Get
-	$userData = query('select', 'users', ['username' => $_SESSION['username']]);
+	$userData = query('select', 'users', ['id' => $_SESSION['id']]);
 	$tokens = $userData[0]['bucket_tokens'];
 	$bucketLastUpdate = $userData[0]['bucket_last_update'];
 
@@ -75,8 +79,8 @@ function rateLimitAccount($requestedTokens) {
 
 	// Update
 	$db = new PDO('sqlite:' . DB_PATH);
-	$stmt = $db->prepare('UPDATE users SET bucket_tokens = :bucket_tokens, bucket_last_update = :bucket_last_update WHERE username = :username');
-	$stmt->bindValue(':username', $_SESSION['username']);
+	$stmt = $db->prepare('UPDATE users SET bucket_tokens = :bucket_tokens, bucket_last_update = :bucket_last_update WHERE id = :id');
+	$stmt->bindValue(':id', $_SESSION['id']);
 	$stmt->bindValue(':bucket_tokens', $tokens);
 	$stmt->bindValue(':bucket_last_update', time());
 	$stmt->execute();

+ 4 - 4
fn/common.php

@@ -20,11 +20,11 @@ function output($code, $msg = '', $logs = ['']) {
 function processForm($requireLogin = true) {
 	if (http_response_code() !== 200)
 		return false;
-	if (empty($_POST) AND $requireLogin AND !isset($_SESSION['username']))
+	if (empty($_POST) AND $requireLogin AND !isset($_SESSION['id']))
 		echo '<p>Ce formulaire ne sera pas accepté car il faut <a class="auth" href="' . redirUrl('auth/login') . '">se connecter</a> avant.</p>';
 	if (empty($_POST))
 		return false;
-	if ($requireLogin AND !isset($_SESSION['username']))
+	if ($requireLogin AND !isset($_SESSION['id']))
 		output(403, 'Vous devez être connecté·e pour effectuer cette action.');
 	return true;
 }
@@ -152,11 +152,11 @@ if (!file_exists(SECRET_KEY_FILE)) {
 define('SECRET_KEY', file_get_contents(SECRET_KEY_FILE));
 function getAuthToken() {
 	$salt = bin2hex(random_bytes(4));
-	$hash = hash_hmac('sha256', $salt . ($_SESSION['username'] ?? ''), SECRET_KEY);
+	$hash = hash_hmac('sha256', $salt . ($_SESSION['id'] ?? ''), SECRET_KEY);
 	return $salt . '-' .  substr($hash, 0, 32);
 }
 function checkAuthToken($salt, $hash) {
-	$correctProof = substr(hash_hmac('sha256', $salt . $_SESSION['username'], SECRET_KEY), 0, 32);
+	$correctProof = substr(hash_hmac('sha256', $salt . $_SESSION['id'], SECRET_KEY), 0, 32);
 	if (hash_equals($correctProof, $hash) !== true)
 		output(403, 'Preuve incorrecte');
 }

+ 7 - 7
fn/ht.php

@@ -33,15 +33,15 @@ function addSite($username, $siteDir, $domain, $domainType, $protocol) {
 }
 
 function dirsStatuses($domainType, $protocol) {
-	if (isset($_SESSION['username']) !== true)
+	if (isset($_SESSION['id']) !== true)
 		return [];
 	$dbDirs = query('select', 'sites', [
-		'username' => $_SESSION['username'],
+		'username' => $_SESSION['id'],
 		'domain_type' => $domainType,
 		'protocol' => $protocol,
 	], 'site_dir');
 	$dirs = [];
-	foreach (listFsDirs($_SESSION['username']) as $fsDir)
+	foreach (listFsDirs($_SESSION['id']) as $fsDir)
 		$dirs[$fsDir] = in_array($fsDir, $dbDirs);
 	return $dirs;
 }
@@ -50,7 +50,7 @@ function htDeleteSite($dir, $domainType, $protocol) {
 
 	if ($domainType === 'onion') {
 		// Delete Tor config
-		if (unlink(CONF['ht']['tor_config_path'] . '/' . $_SESSION['username'] . '/' . $dir) !== true)
+		if (unlink(CONF['ht']['tor_config_path'] . '/' . $_SESSION['id'] . '/' . $dir) !== true)
 			output(500, 'Failed to delete Tor configuration.');
 
 		// Reload Tor
@@ -59,14 +59,14 @@ function htDeleteSite($dir, $domainType, $protocol) {
 			output(500, 'Failed to reload Tor.');
 
 		// Delete Tor keys
-		exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['rm_path'] . ' --recursive ' . CONF['ht']['tor_keys_path'] . '/' . $_SESSION['username'] . '/' . $dir, $output, $code);
+		exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['rm_path'] . ' --recursive ' . CONF['ht']['tor_keys_path'] . '/' . $_SESSION['id'] . '/' . $dir, $output, $code);
 		if ($code !== 0)
 			output(500, 'Failed to delete Tor keys.');
 	}
 
 	// Delete Nginx config
 	$domain = query('select', 'sites', [
-		'username' => $_SESSION['username'],
+		'username' => $_SESSION['id'],
 		'domain_type' => $domainType,
 		'protocol' => $protocol,
 		'site_dir' => $dir,
@@ -88,7 +88,7 @@ function htDeleteSite($dir, $domainType, $protocol) {
 
 	// Delete from database
 	query('delete', 'sites', [
-		'username' => $_SESSION['username'],
+		'username' => $_SESSION['id'],
 		'domain_type' => $domainType,
 		'protocol' => $protocol,
 		'site_dir' => $dir,

+ 3 - 3
fn/ns.php

@@ -22,7 +22,7 @@ function nsCommonRequirements() {
 		AND isset($_POST['zone'])
 		AND isset($_POST['ttl-value'])
 		AND isset($_POST['ttl-multiplier'])
-		AND isset($_SESSION['username'])
+		AND isset($_SESSION['id'])
 	);
 }
 
@@ -51,7 +51,7 @@ function nsListUserZones($username) {
 function nsCheckZonePossession($zone) {
 	checkAbsoluteDomainFormat($zone);
 
-	if (!in_array($zone, query('select', 'zones', ['username' => $_SESSION['username']], 'zone'), true))
+	if (!in_array($zone, query('select', 'zones', ['username' => $_SESSION['id']], 'zone'), true))
 		output(403, 'You don\'t own this zone on the nameserver.');
 }
 
@@ -69,6 +69,6 @@ function nsDeleteZone($zone) {
 	// Remove from database
 	query('delete', 'zones', [
 		'zone' => $zone,
-		'username' => $_SESSION['username'],
+		'username' => $_SESSION['id'],
 	]);
 }

+ 3 - 3
fn/reg.php

@@ -1,13 +1,13 @@
 <?php
 
-define('SUBDOMAIN_REGEX', '^[a-z0-9]{4,63}$');
+const SUBDOMAIN_REGEX = '^[a-z0-9]{4,63}$';
 
 function regListUserDomains($username) {
 	return query('select', 'registry', ['username' => $username], 'domain');
 }
 
 function regCheckDomainPossession($domain) {
-	if (in_array($domain, regListUserDomains($_SESSION['username']), true) !== true)
+	if (in_array($domain, regListUserDomains($_SESSION['id']), true) !== true)
 		output(403, 'You don\'t own this domain.');
 }
 
@@ -23,6 +23,6 @@ function regDeleteDomain($domain) {
 	// Delete from Niver's database
 	query('delete', 'registry', [
 		'domain' => $domain,
-		'username' => $_SESSION['username'],
+		'username' => $_SESSION['id'],
 	]);
 }

+ 2 - 2
form.ns.php

@@ -18,8 +18,8 @@
 			<select required="" name="zone" id="zone">
 				<option value="" disabled="" selected="">-</option>
 <?php
-if (isset($_SESSION['username']))
-	foreach (nsListUserZones($_SESSION['username']) as $zone)
+if (isset($_SESSION['id']))
+	foreach (nsListUserZones($_SESSION['id']) as $zone)
 		echo "<option value='" . $zone . "'>" . $zone . "</option>";
 ?>
 

+ 4 - 0
pages.php

@@ -26,6 +26,10 @@ define('PAGES', [
 			'title' => 'Changer la clé de passe',
 			'description' => 'Changer la chaîne de caractères permettant de vous authentifier.',
 		],
+		'username' => [
+			'title' => 'Changer l\'identifiant',
+			'description' => 'Changer la chaîne de caractères permettant d\'identifier votre compte.',
+		],
 		'logout' => [
 			'title' => 'Déconnexion',
 			'description' => 'Terminer la session et effacer ses cookies',

+ 4 - 4
pages/auth/index.php

@@ -1,7 +1,7 @@
 <?php displayIndex(); ?>
 <p>
-<?php if (isset($_SESSION['username'])) { ?>
-	Vous utilisez actuellement un compte <?= (($_SESSION['type'] === 'trusted') ? 'confiancé' : 'de test') ?>.
+<?php if (isset($_SESSION['id'])) { ?>
+	Vous utilisez actuellement un compte <?= (($_SESSION['type'] === 'trusted') ? 'confiancé' : 'de test') ?>. Son identifiant interne est <code><?= $_SESSION['id'] ?></code>.
 <?php } else { ?>
 	Vous n'utilisez actuellement aucun compte.
 <?php } ?>
@@ -10,7 +10,7 @@
 <h2>Types de comptes</h2>
 
 <dl>
-	<dt>De test</dt>
+	<dt><span aria-hidden="true">⏳ </span>De test</dt>
 	<dd>
 		C'est le type de compte par défaut, avec des fonctionnalités limitées pour éviter les abus&nbsp;:
 		<ul>
@@ -19,7 +19,7 @@
 			<li>Certificat Let's Encrypt de test</li>
 		</ul>
 	</dd>
-	<dt>Confiancé</dt>
+	<dt><span aria-hidden="true">👤 </span>Confiancé</dt>
 	<dd>
 		C'est originellement un compte de test mais qui a été confiancé par ane administrataire, et qui a pour but d'être utilisé de façon stable&nbsp;:
 		<ul>

+ 9 - 7
pages/auth/login.php

@@ -5,20 +5,22 @@ if (processForm(false)) {
 
 	checkUsernameFormat($_POST['username']);
 
-	$internal_username = hash('sha256', $_POST['username']);
+	$username = hashUsername($_POST['username']);
 
-	if (userExist($internal_username) !== true)
+	if (usernameExists($username) !== true)
 		output(403, 'Connexion impossible : ce compte n\'existe pas.');
 
-	if (checkPassword($internal_username, $_POST['password']) !== true)
+	$id = query('select', 'users', ['username' => $username], 'id')[0];
+
+	if (checkPassword($id, $_POST['password']) !== true)
 		output(403, 'Connexion impossible : clé de passe invalide.');
 
-	$_SESSION['username'] = $internal_username;
+	$_SESSION['id'] = $id;
 	$_SESSION['display-username'] = htmlspecialchars($_POST['username']);
-	$_SESSION['type'] = query('select', 'users', ['username' => $internal_username], 'type')[0];
+	$_SESSION['type'] = query('select', 'users', ['id' => $id], 'type')[0];
 
-	if (outdatedPasswordHash($internal_username))
-		changePassword($internal_username, $_POST['password']);
+	if (outdatedPasswordHash($id))
+		changePassword($id, $_POST['password']);
 
 	redir();
 }

+ 7 - 7
pages/auth/password.php

@@ -1,12 +1,12 @@
 <?php
 
 if (processForm()) {
-	checkPasswordFormat($_POST['newPassword']);
+	checkPasswordFormat($_POST['new-password']);
 
-	if (checkPassword($_SESSION['username'], $_POST['currentPassword']) !== true)
+	if (checkPassword($_SESSION['id'], $_POST['current-password']) !== true)
 		output(403, 'Changement impossible : clé de passe invalide.');
 
-	changePassword($_SESSION['username'], $_POST['newPassword']);
+	changePassword($_SESSION['id'], $_POST['new-password']);
 
 	output(200, 'Clé de passe changée.');
 }
@@ -18,11 +18,11 @@ if (processForm()) {
 </p>
 
 <form method="post">
-	<label for="currentPassword">Clé de passe actuelle</label><br>
-	<input required="" autocomplete="current-password" minlength="8" maxlength="1024" pattern="<?= PASSWORD_REGEX ?>" id="currentPassword" name="currentPassword" type="password" placeholder="<?= PLACEHOLDER_PASSWORD ?>"><br>
+	<label for="current-password">Clé de passe actuelle</label><br>
+	<input required="" autocomplete="current-password" minlength="8" maxlength="1024" pattern="<?= PASSWORD_REGEX ?>" id="current-password" name="current-password" type="password" placeholder="<?= PLACEHOLDER_PASSWORD ?>"><br>
 
-	<label for="newPassword">Nouvelle clé de passe</label><br>
-	<input required="" autocomplete="new-password" minlength="8" maxlength="1024" pattern="<?= PASSWORD_REGEX ?>" id="newPassword" name="newPassword" type="password" placeholder="<?= PLACEHOLDER_PASSWORD ?>"><br>
+	<label for="new-password">Nouvelle clé de passe</label><br>
+	<input required="" autocomplete="new-password" minlength="8" maxlength="1024" pattern="<?= PASSWORD_REGEX ?>" id="new-password" name="new-password" type="password" placeholder="<?= PLACEHOLDER_PASSWORD ?>"><br>
 
 	<input type="submit">
 </form>

+ 11 - 8
pages/auth/register.php

@@ -5,15 +5,18 @@ if (processForm(false)) {
 
 	checkUsernameFormat($_POST['username']);
 
-	$internal_username = hash('sha256', $_POST['username']);
+	$username = hashUsername($_POST['username']);
 
-	if (userExist($internal_username) !== false)
+	if (usernameExists($username) !== false)
 		output(403, 'Ce nom de compte est déjà utilisé.');
 
 	rateLimit();
 
+	$id = hash('sha256', random_bytes(32));
+
 	insert('users', [
-		'username' => $internal_username,
+		'id' => $id,
+		'username' => $username,
 		'password' => hashPassword($_POST['password']),
 		'registration_date' => date('Y-m-d H:i:s'),
 		'bucket_tokens' => 0,
@@ -23,22 +26,22 @@ if (processForm(false)) {
 
 	// Setup SFTP directory
 	umask(0002);
-	if (mkdir(CONF['ht']['ht_path'] . '/' . $internal_username, 0775) !== true)
+	if (mkdir(CONF['ht']['ht_path'] . '/' . $id, 0775) !== true)
 		output(500, 'Can\'t create user directory.');
-	exec(CONF['ht']['sudo_path'] . ' ' . CONF['ht']['chgrp_path'] . ' ' . CONF['ht']['sftpgo_group'] . ' ' . CONF['ht']['ht_path'] . '/' . $internal_username . ' --no-dereference', result_code: $code);
+	exec(CONF['ht']['sudo_path'] . ' ' . CONF['ht']['chgrp_path'] . ' ' . CONF['ht']['sftpgo_group'] . ' ' . CONF['ht']['ht_path'] . '/' . $id . ' --no-dereference', result_code: $code);
 	if ($code !== 0)
 		output(500, 'Can\'t change user directory group.');
 
 	// Setup Tor config directory
-	if (mkdir(CONF['ht']['tor_config_path'] . '/' . $internal_username, 0755) !== true)
+	if (mkdir(CONF['ht']['tor_config_path'] . '/' . $id, 0755) !== true)
 		output(500, 'Can\'t create Tor config directory.');
 
 	// Setup Tor keys directory
-	exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['mkdir_path'] . ' --mode=0700 ' . CONF['ht']['tor_keys_path'] . '/' . $internal_username, result_code: $code);
+	exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['mkdir_path'] . ' --mode=0700 ' . CONF['ht']['tor_keys_path'] . '/' . $id, result_code: $code);
 	if ($code !== 0)
 		output(500, 'Can\'t create Tor keys directory.');
 
-	$_SESSION['username'] = $internal_username;
+	$_SESSION['id'] = $id;
 	$_SESSION['display-username'] = htmlspecialchars($_POST['username']);
 	$_SESSION['type'] = 'testing';
 

+ 8 - 8
pages/auth/unregister.php

@@ -4,37 +4,37 @@ if (processForm()) {
 	if (!isset($_POST['delete']))
 		output(403, 'Il faut confirmer la suppression du compte');
 
-	foreach (query('select', 'registry', ['username' => $_SESSION['username']], 'domain') as $domain)
+	foreach (query('select', 'registry', ['username' => $_SESSION['id']], 'domain') as $domain)
 		regDeleteDomain($domain);
 
-	foreach (query('select', 'zones', ['username' => $_SESSION['username']], 'zone') as $zone)
+	foreach (query('select', 'zones', ['username' => $_SESSION['id']], 'zone') as $zone)
 		nsDeleteZone($zone);
 
 	foreach (query('select', 'sites', [
-		'username' => $_SESSION['username'],
+		'username' => $_SESSION['id'],
 		'domain_type' => 'onion',
 		'protocol' => 'http',
 	], 'site_dir') as $dir)
 		htDeleteSite($dir, domainType: 'onion', protocol: 'http');
 
 	foreach (query('select', 'sites', [
-		'username' => $_SESSION['username'],
+		'username' => $_SESSION['id'],
 		'domain_type' => 'dns',
 		'protocol' => 'http',
 	], 'site_dir') as $dir)
 		htDeleteSite($dir, domainType: 'dns', protocol: 'http');
 
-	exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['rm_path'] . ' --recursive ' . CONF['ht']['tor_keys_path'] . '/' . $_SESSION['username'], result_code: $code);
+	exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['rm_path'] . ' --recursive ' . CONF['ht']['tor_keys_path'] . '/' . $_SESSION['id'], result_code: $code);
 	if ($code !== 0)
 		output(500, 'Can\'t remove Tor keys directory.');
 
-	removeDirectory(CONF['ht']['tor_config_path'] . '/' . $_SESSION['username']);
+	removeDirectory(CONF['ht']['tor_config_path'] . '/' . $_SESSION['id']);
 
-	exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['sftpgo_user'] .  ' ' . CONF['ht']['rm_path'] . ' --recursive ' . CONF['ht']['ht_path'] . '/' . $_SESSION['username'], result_code: $code);
+	exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['sftpgo_user'] .  ' ' . CONF['ht']['rm_path'] . ' --recursive ' . CONF['ht']['ht_path'] . '/' . $_SESSION['id'], result_code: $code);
 	if ($code !== 0)
 		output(500, 'Can\'t remove user\'s directory.');
 
-	query('delete', 'users', ['username' => $_SESSION['username']]);
+	query('delete', 'users', ['id' => $_SESSION['id']]);
 
 	require 'logout.php';
 

+ 36 - 0
pages/auth/username.php

@@ -0,0 +1,36 @@
+<?php
+
+if (processForm()) {
+	checkUsernameFormat($_POST['new-username']);
+
+	$username = hashUsername($_POST['new-username']);
+
+	if (usernameExists($username) !== false)
+		output(403, 'Ce nom de compte est déjà utilisé.');
+
+	$db = new PDO('sqlite:' . DB_PATH);
+
+	$stmt = $db->prepare('UPDATE users SET username = :username WHERE id = :id');
+
+	$stmt->bindValue(':id', $_SESSION['id']);
+	$stmt->bindValue(':username', $username);
+
+	$stmt->execute();
+
+	$_SESSION['display-username'] = htmlspecialchars($_POST['new-username']);
+
+	output(200, 'Identifiant changé.');
+}
+
+?>
+
+<p>
+	Vous pouvez ici changer l'identifiant permettant d'accéder à votre compte Niver.
+</p>
+
+<form method="post">
+	<label for="new-username">Nouvel identifiant</label><br>
+	<input required="" autocomplete="new-username" minlength="1" maxlength="1024" pattern="<?= USERNAME_REGEX ?>" id="new-username" name="new-username" type="text" placeholder="<?= PLACEHOLDER_USERNAME ?>"><br>
+
+	<input type="submit">
+</form>

+ 2 - 2
pages/ht/add-http-dns.php

@@ -31,7 +31,7 @@ if (processForm()) {
 
 	rateLimit();
 
-	addSite($_SESSION['username'], $_POST['dir'], $_POST['domain'], 'dns', 'http');
+	addSite($_SESSION['id'], $_POST['dir'], $_POST['domain'], 'dns', 'http');
 
 	exec('2>&1 ' . CONF['ht']['sudo_path'] . ' ' . CONF['ht']['certbot_path'] . ' certonly' . (($_SESSION['type'] === 'trusted') ? '' : ' --test-cert') . ' --key-type rsa --rsa-key-size 3072 --webroot --webroot-path /srv/niver/acme --domain ' . $_POST['domain'], $output, $returnCode);
 	if ($returnCode !== 0)
@@ -41,7 +41,7 @@ if (processForm()) {
 	listen [' . CONF['ht']['ipv6_listen_address'] . ']:' . CONF['ht']['https_port'] . ' ssl http2;
 	listen ' . CONF['ht']['ipv4_listen_address'] . ':' . CONF['ht']['https_port'] . ' ssl http2;
 	server_name ' . $_POST['domain'] . ';
-	root ' . CONF['ht']['ht_path'] . '/' . $_SESSION['username'] . '/' . $_POST['dir'] . ';
+	root ' . CONF['ht']['ht_path'] . '/' . $_SESSION['id'] . '/' . $_POST['dir'] . ';
 
 	ssl_certificate /etc/letsencrypt/live/' . $_POST['domain'] . '/fullchain.pem;
 	ssl_certificate_key /etc/letsencrypt/live/' . $_POST['domain'] . '/privkey.pem;

+ 5 - 5
pages/ht/add-http-onion.php

@@ -7,10 +7,10 @@ if (processForm()) {
 	rateLimit();
 
 	// Add Tor config
-	$torConf = 'HiddenServiceDir ' . CONF['ht']['tor_keys_path'] . '/' . $_SESSION['username'] . '/' . $_POST['dir'] . '/
+	$torConf = 'HiddenServiceDir ' . CONF['ht']['tor_keys_path'] . '/' . $_SESSION['id'] . '/' . $_POST['dir'] . '/
 	HiddenServicePort 80 [::1]:' . CONF['ht']['internal_onion_http_port'] . '
 	';
-	if (file_put_contents(CONF['ht']['tor_config_path'] . '/' . $_SESSION['username'] . '/' . $_POST['dir'], $torConf) === false)
+	if (file_put_contents(CONF['ht']['tor_config_path'] . '/' . $_SESSION['id'] . '/' . $_POST['dir'], $torConf) === false)
 		output(500, 'Failed to write new Tor configuration.');
 
 	// Reload Tor
@@ -19,19 +19,19 @@ if (processForm()) {
 		output(500, 'Failed to reload Tor.');
 
 	// Get the address generated by Tor
-	exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['cat_path'] . ' ' . CONF['ht']['tor_keys_path'] . '/' . $_SESSION['username'] . '/' . $_POST['dir'] . '/hostname', $output);
+	exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['cat_path'] . ' ' . CONF['ht']['tor_keys_path'] . '/' . $_SESSION['id'] . '/' . $_POST['dir'] . '/hostname', $output);
 	$onion = $output[0];
 	if (preg_match('/^[0-9a-z]{56}\.onion$/D', $onion) !== 1)
 		output(500, 'No onion address found.');
 
 	// Store it in the database
-	addSite($_SESSION['username'], $_POST['dir'], $onion, 'onion', 'http');
+	addSite($_SESSION['id'], $_POST['dir'], $onion, 'onion', 'http');
 
 	// Add Nginx config
 	$nginxConf = 'server {
 		listen [::1]:' . CONF['ht']['internal_onion_http_port'] . ';
 		server_name ' . $onion . ';
-		root ' . CONF['ht']['ht_path'] . '/' . $_SESSION['username'] . '/' . $_POST['dir'] . ';
+		root ' . CONF['ht']['ht_path'] . '/' . $_SESSION['id'] . '/' . $_POST['dir'] . ';
 
 		include inc/ht-onion.conf;
 	}

+ 3 - 3
pages/ht/index.php

@@ -9,7 +9,7 @@
 	<dl>
 <?php
 
-$sites = query('select', 'sites', ['username' => $_SESSION['username'] ?? '']);
+$sites = query('select', 'sites', ['username' => $_SESSION['id'] ?? '']);
 if ($sites === [])
 	echo '	<p>Ce compte n\'héberge aucun site sur cette instance.<p>' . LF;
 else {
@@ -71,7 +71,7 @@ echo (($quotaSize >> 30) >= 1) ? $quotaSize >> 30 . ' ' . linkToDocs('units', '<
 	<section>
 		<h3>Se connecter au serveur</h3>
 
-		<a href="sftp://<?= isset($_SESSION['username']) ? $_SESSION['username'] : '&lt;username&gt;'; ?>@<?= CONF['ht']['sftp_domain'] ?>:<?= CONF['ht']['public_sftp_port'] ?>/">sftp://<?= isset($_SESSION['username']) ? $_SESSION['username'] : '&lt;username&gt;'; ?>@<?= CONF['ht']['sftp_domain'] ?>:<?= CONF['ht']['public_sftp_port'] ?>/</a>
+		<a href="sftp://<?= isset($_SESSION['display-username']) ? $_SESSION['display-username'] : '&lt;username&gt;'; ?>@<?= CONF['ht']['sftp_domain'] ?>:<?= CONF['ht']['public_sftp_port'] ?>/">sftp://<?= isset($_SESSION['display-username']) ? $_SESSION['display-username'] : '&lt;username&gt;'; ?>@<?= CONF['ht']['sftp_domain'] ?>:<?= CONF['ht']['public_sftp_port'] ?>/</a>
 
 		<dl>
 			<dt>Serveur</dt>
@@ -88,7 +88,7 @@ echo (($quotaSize >> 30) >= 1) ? $quotaSize >> 30 . ' ' . linkToDocs('units', '<
 			</dd>
 			<dt>Utilisataire</dt>
 			<dd>
-				<code><?= isset($_SESSION['username']) ? $_SESSION['username'] : '&lt;username&gt;'; ?></code>
+				<code><?= isset($_SESSION['display-username']) ? $_SESSION['display-username'] : '&lt;username&gt;'; ?></code>
 			</dd>
 			<dt>Clé de passe</dt>
 			<dd>

+ 2 - 2
pages/ns/edit.php

@@ -70,8 +70,8 @@ if (processForm() AND isset($_POST['zone-content'])) { // Update zone
 	<select required="" name="zone" id="zone">
 		<option value="" disabled="" selected="">-</option>
 <?php
-if (isset($_SESSION['username']))
-	foreach (nsListUserZones($_SESSION['username']) as $zone)
+if (isset($_SESSION['id']))
+	foreach (nsListUserZones($_SESSION['id']) as $zone)
 		echo '		<option value="' . $zone . '">' . $zone . '</option>' . LF;
 ?>
 	</select>

+ 1 - 1
pages/ns/index.php

@@ -19,7 +19,7 @@ foreach (CONF['ns']['servers'] as $server)
 
 <?php
 
-$zones = query('select', 'zones', ['username' => $_SESSION['username'] ?? ''], 'zone');
+$zones = query('select', 'zones', ['username' => $_SESSION['id'] ?? ''], 'zone');
 if ($zones === [])
 	echo '<p>Ce compte n\'héberge aucune zone sur cette instance.<p>' . LF;
 else {

+ 2 - 2
pages/ns/print.php

@@ -13,8 +13,8 @@
 	<select required="" name="zone" id="zone">
 		<option value="" disabled="" selected="">-</option>
 <?php
-if (isset($_SESSION['username']))
-	foreach (nsListUserZones($_SESSION['username']) as $zone)
+if (isset($_SESSION['id']))
+	foreach (nsListUserZones($_SESSION['id']) as $zone)
 		echo '		<option value="' . $zone . '">' . $zone . '</option>' . LF;
 ?>
 	</select>

+ 1 - 1
pages/ns/zone-add.php

@@ -22,7 +22,7 @@ if (processForm()) {
 
 	insert('zones', [
 		'zone' => $_POST['domain'],
-		'username' => $_SESSION['username'],
+		'username' => $_SESSION['id'],
 	]);
 
 	$knotZonePath = CONF['ns']['knot_zones_path'] . '/' . $_POST['domain'] . 'zone';

+ 2 - 2
pages/ns/zone-del.php

@@ -15,8 +15,8 @@ if (processForm()) {
 	<select required="" name="zone" id="zone">
 		<option value="" disabled="" selected="">-</option>
 <?php
-if (isset($_SESSION['username']))
-	foreach (nsListUserZones($_SESSION['username']) as $zone)
+if (isset($_SESSION['id']))
+	foreach (nsListUserZones($_SESSION['id']) as $zone)
 		echo '		<option value="' . $zone . '">' . $zone . '</option>' . LF;
 ?>
 	</select>

+ 2 - 2
pages/reg/ds.php

@@ -1,7 +1,7 @@
 <?php
 
-if (isset($_SESSION['username']))
-	$domains = regListUserDomains($_SESSION['username']);
+if (isset($_SESSION['id']))
+	$domains = regListUserDomains($_SESSION['id']);
 else
 	$domains = [];
 

+ 2 - 2
pages/reg/glue.php

@@ -42,8 +42,8 @@ if (processForm()) {
 			<select required="" name="suffix" id="suffix">
 				<option value="" disabled="" selected="">---</option>
 <?php
-if (isset($_SESSION['username']))
-	foreach(regListUserDomains($_SESSION['username']) as $suffix)
+if (isset($_SESSION['id']))
+	foreach(regListUserDomains($_SESSION['id']) as $suffix)
 		echo '		<option value="' . $suffix . '">' . $suffix . '</option>' . LF;
 ?>
 			</select>

+ 1 - 1
pages/reg/index.php

@@ -8,7 +8,7 @@
 
 <?php
 
-$domains = query('select', 'registry', ['username' => $_SESSION['username'] ?? ''], 'domain');
+$domains = query('select', 'registry', ['username' => $_SESSION['id'] ?? ''], 'domain');
 if ($domains === [])
 	echo '<p>Ce compte n\'a aucun domaine enregistré sur <code>' . CONF['reg']['registry'] . '</code><p>' . LF;
 else {

+ 2 - 2
pages/reg/ns.php

@@ -32,8 +32,8 @@ if (processForm()) {
 	<select required="" name="domain" id="domain">
 		<option value="" disabled="" selected="">---</option>
 <?php
-if (isset($_SESSION['username']))
-	foreach (regListUserDomains($_SESSION['username']) as $domain)
+if (isset($_SESSION['id']))
+	foreach (regListUserDomains($_SESSION['id']) as $domain)
 		echo '		<option value="' . $domain . '">' . $domain . '</option>' . LF;
 ?>
 	</select>

+ 2 - 2
pages/reg/print.php

@@ -3,8 +3,8 @@
 	<select required="" name="domain" id="domain">
 		<option value="" disabled="" selected="">-</option>
 <?php
-if (isset($_SESSION['username']))
-	foreach (regListUserDomains($_SESSION['username']) as $domain)
+if (isset($_SESSION['id']))
+	foreach (regListUserDomains($_SESSION['id']) as $domain)
 		echo '		<option value="' . $domain . '">' . $domain . '</option>' . LF;
 ?>
 	</select>

+ 1 - 1
pages/reg/register.php

@@ -16,7 +16,7 @@ if (processForm()) {
 
 	insert('registry', [
 		'domain' => $domain,
-		'username' => $_SESSION['username'],
+		'username' => $_SESSION['id'],
 		'last_renewal' => date('Y-m-d H:i:s'),
 	]);
 

+ 2 - 2
pages/reg/unregister.php

@@ -20,8 +20,8 @@ if (processForm()) {
 	<select required="" name="domain" id="domain">
 		<option value="" disabled="" selected="">---</option>
 <?php
-if (isset($_SESSION['username']))
-	foreach(regListUserDomains($_SESSION['username']) as $domain)
+if (isset($_SESSION['id']))
+	foreach(regListUserDomains($_SESSION['id']) as $domain)
 		echo '		<option value="' . $domain . '">' . $domain . '</option>' . LF;
 ?>
 	</select>

+ 2 - 2
router.php

@@ -91,8 +91,8 @@ foreach (glob('css/*.css') as $cssPath)
 	<body>
 		<header>
 			<p>
-<?php if (isset($_SESSION['username'])) { ?>
-				<?= ($_SESSION['type'] === 'trusted') ? '<span title="Compte confiancé">👤</span>' : '<span title="Compte de test">⏳</span>' ?> <strong><?= $_SESSION['display-username'] ?></strong> <a class="auth" href="<?= CONF['common']['prefix'] ?>/auth/logout">Se déconnecter</a>
+<?php if (isset($_SESSION['id'])) { ?>
+				<?= ($_SESSION['type'] === 'trusted') ? '<span title="Compte confiancé">👤 </span>' : '<span title="Compte de test">⏳ </span>' ?><strong><?= $_SESSION['display-username'] ?></strong> <a class="auth" href="<?= CONF['common']['prefix'] ?>/auth/logout">Se déconnecter</a>
 <?php } else { ?>
 				<span aria-hidden="true">👻 </span><em>Anonyme</em> <a class="auth" href="<?= redirUrl('auth/login') ?>">Se connecter</a>
 <?php } ?>

+ 6 - 4
sftpgo-auth.php

@@ -4,15 +4,17 @@ require 'router.php';
 
 $auth_data = json_decode(file_get_contents('php://input'), true);
 
-$internal_username = hash('sha256', $auth_data['username']);
+$username = hashUsername($auth_data['username']);
 
-if (userExist($internal_username) === true AND checkPassword($internal_username, $auth_data['password']) === true) {
+$id = query('select', 'users', ['username' => $username], 'id')[0];
+
+if (usernameExists($username) === true AND checkPassword($id, $auth_data['password']) === true) {
 	echo '
 	{
 		"status": 1,
 		"username": ' . json_encode($auth_data['username']) . ',
-		"home_dir": "' . CONF['ht']['ht_path'] . '/' . $internal_username . '",
-		"quota_size": ' . ((query('select', 'users', ['username' => $internal_username], 'type')[0] === 'trusted') ? CONF['ht']['user_quota_trusted'] : CONF['ht']['user_quota_testing']) . ',
+		"home_dir": "' . CONF['ht']['ht_path'] . '/' . $id . '",
+		"quota_size": ' . ((query('select', 'users', ['id' => $id], 'type')[0] === 'trusted') ? CONF['ht']['user_quota_trusted'] : CONF['ht']['user_quota_testing']) . ',
 		"permissions": {
 			"/": [
 				"*"