| Server IP : 35.80.110.71 / Your IP : 216.73.216.221 Web Server : Apache/2.4.58 (Ubuntu) System : Linux ip-172-31-21-44 6.17.0-1019-aws #19~24.04.1-Ubuntu SMP Tue Jun 23 18:53:06 UTC 2026 x86_64 User : ubuntu ( 1000) PHP Version : 8.3.31 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/splitstream/releases/20260611102328/app/Models/ |
Upload File : |
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
class ApiKey extends Model
{
use HasUlids;
public const KIND_SERVER = 'server';
public const KIND_CLIENT = 'client';
protected $fillable = [
'workspace_id',
'environment_id',
'name',
'kind',
'prefix',
'key_hash',
'key_lookup_hmac',
'key_argon2_hash',
'last_four',
];
protected $hidden = ['key_hash', 'key_lookup_hmac', 'key_argon2_hash'];
protected function casts(): array
{
return [
'last_used_at' => 'datetime',
'revoked_at' => 'datetime',
];
}
public function workspace(): BelongsTo
{
return $this->belongsTo(Workspace::class);
}
public function environment(): BelongsTo
{
return $this->belongsTo(Environment::class);
}
/**
* Mint a key and return [model, plaintext].
*
* Format: ss_{srv|clt}_{live|test}_{32 random chars}
*
* Storage:
* - key_lookup_hmac = HMAC-SHA256(plaintext, APP_KEY_PEPPER) — indexed
* for the request-path constant-time lookup.
* - key_argon2_hash = Argon2id(plaintext) — defense in depth; verified
* only at Octane worker startup (per-worker cache).
* - key_hash = sha256(plaintext) — legacy column for the v0.1 rollback
* window; drop in v0.2.
*/
public static function mint(
Workspace $workspace,
string $kind,
string $env = 'live',
?Environment $environment = null,
?string $name = null,
): array {
if (! in_array($kind, [self::KIND_SERVER, self::KIND_CLIENT], true)) {
throw new \InvalidArgumentException("kind must be 'server' or 'client'");
}
if (! in_array($env, ['live', 'test'], true)) {
throw new \InvalidArgumentException("env must be 'live' or 'test'");
}
$kindCode = $kind === self::KIND_SERVER ? 'srv' : 'clt';
$prefix = "ss_{$kindCode}_{$env}_";
$random = Str::random(32);
$plaintext = $prefix.$random;
$pepper = config('app.key_pepper', env('APP_KEY_PEPPER', ''));
if ($pepper === '') {
// Fail loudly in non-test environments — silent fallback to a
// missing pepper is the worst kind of silent bug.
if (app()->environment() !== 'testing') {
throw new \RuntimeException(
'APP_KEY_PEPPER is empty — key minting is refused. '
.'Generate with `openssl rand -hex 32` and set in .env.'
);
}
$pepper = 'test-pepper-for-testing-only';
}
$apiKey = static::create([
'workspace_id' => $workspace->id,
'environment_id' => $environment?->id,
'name' => $name,
'kind' => $kind,
'prefix' => $prefix,
'key_hash' => hash('sha256', $plaintext),
'key_lookup_hmac' => hash_hmac('sha256', $plaintext, $pepper),
'key_argon2_hash' => password_hash($plaintext, PASSWORD_ARGON2ID),
'last_four' => substr($plaintext, -4),
]);
return [$apiKey, $plaintext];
}
/**
* Constant-time HMAC lookup. Used on every request path under Octane.
* NEVER compute Argon2 in this path — see docs/architecture/api-keys.md.
*/
public static function findByPlaintext(?string $plaintext): ?self
{
if (! is_string($plaintext) || $plaintext === '') {
return null;
}
$pepper = config('app.key_pepper', env('APP_KEY_PEPPER', ''));
if ($pepper === '') {
return null;
}
$hmac = hash_hmac('sha256', $plaintext, $pepper);
return static::query()
->whereNull('revoked_at')
->where('key_lookup_hmac', $hmac)
->first();
}
public function isServer(): bool
{
return $this->kind === self::KIND_SERVER;
}
public function isClient(): bool
{
return $this->kind === self::KIND_CLIENT;
}
}