60 lines
1.9 KiB
PHP
60 lines
1.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Illuminate\Support\Str;
|
|
|
|
/**
|
|
* Provisions a per-device MQTT credential at onboarding. Writes a mosquitto
|
|
* PBKDF2-SHA512 ($7$) password line and touches the reload trigger so the broker
|
|
* re-reads it live (see docker/mosquitto/entrypoint.sh). The device is then bound to its
|
|
* own topic prefix by the `pattern %u` ACL.
|
|
*/
|
|
class MqttCredentialProvisioner
|
|
{
|
|
private const ITERATIONS = 101;
|
|
|
|
private string $passwdFile;
|
|
|
|
private string $reloadFile;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->passwdFile = base_path('docker/mosquitto/config/passwd');
|
|
$this->reloadFile = base_path('docker/mosquitto/config/reload');
|
|
}
|
|
|
|
/** Provision (or rotate) a credential for $username. Returns the plaintext password (show once). */
|
|
public function provision(string $username): string
|
|
{
|
|
$password = Str::random(20);
|
|
$this->writeUser($username, $username.':'.$this->hash($password));
|
|
$this->triggerReload();
|
|
|
|
return $password;
|
|
}
|
|
|
|
private function hash(string $password): string
|
|
{
|
|
$salt = random_bytes(12);
|
|
$derived = hash_pbkdf2('sha512', $password, $salt, self::ITERATIONS, 64, true);
|
|
|
|
return '$7$'.self::ITERATIONS.'$'.base64_encode($salt).'$'.base64_encode($derived);
|
|
}
|
|
|
|
private function writeUser(string $username, string $line): void
|
|
{
|
|
$lines = is_file($this->passwdFile) ? file($this->passwdFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) : [];
|
|
$lines = array_values(array_filter($lines, fn ($l) => ! str_starts_with($l, $username.':')));
|
|
$lines[] = $line;
|
|
|
|
file_put_contents($this->passwdFile, implode("\n", $lines)."\n", LOCK_EX);
|
|
@chmod($this->passwdFile, 0644); // world-readable so the mosquitto user can read it
|
|
}
|
|
|
|
private function triggerReload(): void
|
|
{
|
|
file_put_contents($this->reloadFile, (string) now()->getTimestampMs());
|
|
}
|
|
}
|