Tajné hodnoty v konfigurácii a fasádach
API kľúče, podpisové tajomstvá, heslá a sady kľúčov potrebujú rovnako prísnu kontrolu ako každé iné nastavenie — nesprávne nastavená tajná hodnota sa však nikdy nesmie dostať do chybovej správy, stack trace ani do nástroja na sledovanie chýb. Toolkit pokrýva obe miesta, cez ktoré tajná hodnota balíkom prechádza: čítače konfigurácie aj fasádu.
Čítače tajných hodnôt
secret(), requireSecret() a secretList() čítajú ako string(), requireString() a list(), s dvoma rozdielmi. Chyby nikdy neukážu hodnotu: nesprávne nastavenú tajnú hodnotu opíšu len jej typom, napríklad Configuration value [comments.akismet_key] must be a non-empty string, [int] given., a položka, ktorú $each odmietne, znie must contain only valid items, [string] given. Hodnota nie je v správe ani v trace, nech ju nesie ktorákoľvek trieda výnimky (Config::for() / Config::using()). A predvolená hodnota z balíka neexistuje, pretože pre tajnú hodnotu žiadna rozumná nie je: secret() pri nenastavenom kľúči vráti null a secretList() vráti [], takže o tom, čo znamená „nenakonfigurované“, rozhodne volajúci; requireSecret() vyhodí is required but missing. Zadaný reťazec sa vráti tak, ako je, nikdy neorezaný:
use RoundlyConsulting\PackageToolkit\Support\Config;
// Required: not set throws "is required but missing"
$apiKey = Config::requireSecret('comments.akismet_key');
// Optional: not set → null, and the caller decides what "not configured" means
$apiKey = Config::secret('comments.akismet_key');
// config('comments.akismet_key') === 12345 — misconfigured, and the message names the type only:
// Configuration value [comments.akismet_key] must be a non-empty string, [int] given.| Hodnota v konfigurácii | secret() | requireSecret() | secretList() |
|---|---|---|---|
| Reťazec, napr. ' key ' | ' key ' — tak, ako je, nikdy neorezaný | ' key ' | ['key'] — položky orezané |
'a, b' | 'a, b' | 'a, b' | ['a', 'b'] |
['a', 'b'] | Vyhodí [array] given. | Vyhodí [array] given. | ['a', 'b'] |
| Nenastavená: chýbajúca, null, '' alebo len medzery | null | Vyhodí is required but missing. | [] |
',' | ',' | ',' | [] |
| [] alebo [' ', ''] | Vyhodí [array] given. | Vyhodí [array] given. | [] |
| Int, float, bool, objekt | Vyhodí [int] / [float] / [bool] / [ClassName] given. | Rovnako | Vyhodí must be a list of strings …, [int] given. |
| Pole s položkou, ktorá nie je reťazcom | Vyhodí [array] given. | Rovnako | Vyhodí must contain only string items, [int] given. |
Sady kľúčov
secretList() spracúva hodnotu ako list(): publikované pole alebo zoznam z env oddelený čiarkami, položky orezané, prázdne vypustené, kľúče zahodené. Tajná hodnota s čiarkou preto nemôže byť v zozname z env — publikujte namiesto toho konfiguračné pole. $each dostane každú položku, preto jej parameter označte #[SensitiveParameter]: rámec vašej vlastnej closure toolkit skryť nedokáže a closure, ktorá vyhodí výnimku, by inak položku dostala do trace:
use RoundlyConsulting\PackageToolkit\Support\Config;
use SensitiveParameter;
$keys = Config::secretList('comments.signing_keys', static fn (#[SensitiveParameter] string $key): bool => strlen($key) >= 32);
$current = $keys[0] ?? throw new RuntimeException('Set COMMENTS_SIGNING_KEYS.');Vlastné správy okolo tajnej hodnoty
Každá továrenská metóda InvalidConfigurationException opíše hodnotu zabalenú do SensitiveParameterValue z PHP typom toho, čo obaľuje, nikdy jej obsahom. Využite to, keď váš balík skladá vlastnú správu okolo tajnej hodnoty:
use RoundlyConsulting\PackageToolkit\Exceptions\InvalidConfigurationException;
use SensitiveParameterValue;
throw InvalidConfigurationException::notAString($key, new SensitiveParameterValue($value)); // "[int] given."Hodnoty v rámcoch stack trace
Každá výnimka si zaznamená argumenty všetkých funkcií na zásobníku, pokiaľ ich v produkcii neodstráni zend.exception_ignore_args — a nástroje na sledovanie chýb, ktoré zbierajú argumenty rámcov, print_r($e) či vlastné loggery trace ich serializujú. Každý parameter, ktorý nesie konfiguračnú hodnotu, má preto #[SensitiveParameter]: $default každého čítača, pole odovzdané do Config::for() / ConfigValidator::forArray() aj hodnota v každej továrenskej metóde InvalidConfigurationException. Nesprávne nastavená hodnota sa ukáže v správe bežných čítačov, ktoré ju uvádzajú zámerne, a nikde inde. Čítače tajných hodnôt ju nepustia ani do správy.
Tajné hodnoty odovzdané cez fasádu
PHP nahradí argument s #[SensitiveParameter] objektom SensitiveParameterValue len v rámci funkcie, ktorá atribút deklaruje. Volanie cez bežnú fasádu Laravelu prejde najprv cez Facade::__callStatic($method, $args) a tento rámec nesie všetky argumenty v surovej podobe:
Crypto::constantTimeEquals($known, $userInput);
// #0 CryptoManager->constantTimeEquals(Object(SensitiveParameterValue), Object(SensitiveParameterValue))
// #1 Facade::__callStatic('constantTimeEquals', Array) ← Array holds $known and $userInput rawgetTraceAsString() vypíše pole tohto rámca ako Array, takže tam sa tajná hodnota neukáže. Ukáže sa však všade, kde sa argumenty rámcov serializujú: v nástrojoch na sledovanie chýb, ktoré ich zbierajú, v print_r($e) / var_dump($e) či vo vlastných loggeroch trace. Formátovač logov a chybová stránka Laravelu ukazujú len typy argumentov a produkčné zend.exception_ignore_args = On argumenty rámcov úplne odstráni, takže k úniku dôjde len pri vypnutom nastavení. To isté volanie cez dependency injection bolo skryté už predtým.
Túto medzeru uzavrie trait RedactsSensitiveArguments: v rámci samotnej fasády skryje presne tie argumenty, ktoré metóda koreňového objektu fasády označuje #[SensitiveParameter], a neškodné argumenty nechá viditeľné. Pridajte ho do fasády — nič iné sa nemení:
use Illuminate\Support\Facades\Facade;
use RoundlyConsulting\PackageToolkit\Concerns\RedactsSensitiveArguments;
/**
* @method static bool constantTimeEquals(string $known, string $userInput)
*
* @see CryptoManager
*/
final class Crypto extends Facade
{
use RedactsSensitiveArguments;
protected static function getFacadeAccessor(): string
{
return CryptoManager::class;
}
}Neudržiavate žiadny zoznam metód. Jediným zdrojom sú atribúty #[SensitiveParameter] koreňového objektu, takže nový tajný parameter sa skryje hneď, ako ho označíte.
Čo trait pokrýva
| Volanie | Rámec fasády ukáže |
|---|---|
Crypto::check($secret, $label) | ['check', [Object(SensitiveParameterValue), $label]] |
Crypto::check(label: $label, secret: $secret) | ['label' => $label, 'secret' => Object(SensitiveParameterValue)] |
Crypto::seal($label, ...$parts) | Citlivý variadický parameter: každá zozbieraná časť je skrytá, pozičná aj pomenovaná. |
Crypto::tag($secret, ...$labels) | Neškodný variadický parameter vedľa tajnej hodnoty: tajná hodnota je skrytá, popisy zostanú viditeľné. |
Crypto::check(secrett: $secret) | Preklep v pomenovanom argumente: skrytý (volanie aj tak zlyhá s Unknown named parameter). |
| Metóda bez #[SensitiveParameter] | Všetky argumenty, ako doteraz. |
| Koreňový objekt fasády sa nepodarí získať (kontajner vyhodí výnimku) | Skryté — argumenty sa skryjú ešte predtým, než sa koreňový objekt získa. |
Trait nahrádza Facade::__callStatic(). Citlivé položky vo vlastnom $args zabalí — backtrace ukazuje aktuálnu hodnotu parametra — a ďalej odovzdá nedotknutú kópiu, takže koreňový objekt dostane skutočné hodnoty.
Odkiaľ sa berú pozície
Trait reflexiou preskúma typ accessora: triedu alebo rozhranie, ktoré vracia getFacadeAccessor(). Vyhľadanie prebehne raz pre každú triedu fasády a metódu a výsledok sa uchová v cache na celý proces.
- Rozhrania ako accessor — parametre označte na rozhraní. Fasáda vidí rozhranie, nie triedu, ktorá je naň naviazaná.
- Fake a mocky — fake vymenený cez swap() / fake() alebo Mockery mock zo shouldReceive() dostane skutočné argumenty a zaznamená si ich pre svoje asercie. Rámec fasády zostane skrytý aj vtedy, keď prepísaná metóda vo fake atribút vynechá; o vlastný rámec sa stará fake.
- Čo pokryté nie je — accessor, ktorý nie je class-string (alias v kontajneri, napríklad 'crypto'), a metóda, ktorú typ accessora nedeklaruje (makro, ktoré koreňový objekt preposiela cez __call), sa odovzdajú bez skrytia, presne ako pri bežnej fasáde. Tajné hodnoty vnútri objektov odovzdaných ako argumenty — DTO či objekt s prihlasovacími údajmi — sa ukážu ako samotný objekt; skryť ich obsah je úlohou objektu.
Čo zostáva nezmenené
- Návratové hodnoty a výnimky pochádzajú z koreňového objektu, bez zmeny.
- Prevod skalárnych typov sa zhoduje s Laravelom. Facade::__callStatic() je v súbore bez strict_types, takže Crypto::base64Encode(123) zo striktného súboru prevedie 123 na '123', kým to isté volanie cez dependency injection vyhodí TypeError. Striktnosť určuje súbor, v ktorom je volanie napísané, preto súbor traitu zámerne strict_types nemá a tie isté volania zostanú prijaté.
- „A facade root has not been set.“ sa vyhodí ako doteraz, keď nie je nastavená aplikácia.
- Réžia: približne o 170 ns viac na volanie metódy s tajnou hodnotou a približne o 30 ns pri metóde bez nej, oproti približne 250 ns pri volaní bežnej fasády (PHP 8.4 CLI, zapnutý opcache).
Testovanie skrytia
Rámec fasády čítajte so zapnutými argumentmi rámcov. zend.exception_ignore_args je v produkčnom ini zapnuté — aj na CI runneroch, ktoré ho používajú — a rámec bez argumentov nič nedokazuje:
it('hides the secret in the facade frame', function (): void {
$previous = ini_set('zend.exception_ignore_args', '0');
try {
Crypto::base64UrlDecode('*not-base64*');
} catch (InvalidEncodingException $e) {
$frame = collect($e->getTrace())->firstWhere('function', '__callStatic');
} finally {
ini_set('zend.exception_ignore_args', (string) $previous);
}
expect($frame['args'][0])->toBe('base64UrlDecode')
->and($frame['args'][1][0])->toBeInstanceOf(SensitiveParameterValue::class);
});Prejavte lásku k open source
Tento balík je zadarmo pod licenciou MIT. Ak vám šetrí čas, jednorazový príspevok alebo členstvo na Patreone nám pomôže ho ďalej udržiavať, testovať a dokumentovať.
Ďalšie spôsoby podpory vrátane kryptomienOdoslaním daru súhlasíte s našimi podmienkami prijímania darov.
Chcete to zabudovať do svojho produktu?
Naše balíky integrujeme do zákazkových Laravel a AI riešení. Napíšte nám, na čom pracujete, a ozveme sa do 48 hodín.