homeos/app/Services/MqttCredentialProvisioner.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());
}
}