<?php
/*
* mint-apikey.php <username> <output-file>
*
* Mints an OPNsense API key using OPNSENSE'S OWN MODEL CODE -- the exact path the
* GUI's "Create and download API key for this user" ticket button invokes
* (Auth\Api\UserController::addApiKeyAction -> $user->apikeys->add()).
*
* WHY THIS AND NOT AN OFFLINE GENERATOR: minting the key ourselves would mean
* re-implementing OPNsense's $6$ SHA-512 crypt storage format and keeping it
* byte-compatible forever. A mismatch fails SILENTLY (keys that never
* authenticate). Calling the vendor's own generator has no format to keep in
* sync -- same principle as D-113(a2) itself: call the interface, don't
* re-implement the internals.
*
* The secret is written to <output-file> (0600) and NEVER printed: OPNsense
* stores it as crypt(secret,'$6$'), so creation is the only moment it exists in
* cleartext. Stdout gets lengths only.
*
* Run from /usr/local/opnsense/mvc (load_phalcon.php is relative to it).
*/
require_once('script/load_phalcon.php');
use OPNsense\Core\Config;
use OPNsense\Auth\User;
if ($argc < 3) {
fwrite(STDERR, "usage: mint-apikey.php <username> <output-file>\n");
exit(1);
}
$username = $argv[1];
$outfile = $argv[2];
Config::getInstance()->lock();
$mdl = new User();
$user = $mdl->getUserByName($username);
if ($user == null) {
fwrite(STDERR, "FAIL: no such user: {$username}\n");
exit(1);
}
$new = $user->apikeys->add();
if (empty($new) || empty($new['key']) || empty($new['secret'])) {
fwrite(STDERR, "FAIL: apikeys->add() returned nothing\n");
exit(1);
}
$mdl->serializeToConfig();
Config::getInstance()->save();
$payload = "key=" . $new['key'] . "\nsecret=" . $new['secret'] . "\n";
$fh = fopen($outfile, 'w');
if ($fh === false) {
fwrite(STDERR, "FAIL: cannot write {$outfile}\n");
exit(1);
}
fwrite($fh, $payload);
fclose($fh);
chmod($outfile, 0600);
printf(
"OK: minted for %s -- key=%d chars, secret=%d chars, wrote %d bytes to %s (0600, secret NOT printed)\n",
$username,
strlen($new['key']),
strlen($new['secret']),
strlen($payload),
$outfile
);