First Commit
This commit is contained in:
1 parent
5eb9dd037d
commit
2e62c0e60b
11877 files changed
+2035312
No files matched your search
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* At-rest encryption for seller OAuth tokens.
|
||||
*
|
||||
* Square access/refresh tokens let anyone holding them move money on the
|
||||
* seller's behalf, so — unlike the PayPal merchant IDs we stored in plaintext —
|
||||
* they must not sit readable on disk or in the database. We use AES-256-GCM
|
||||
* (authenticated encryption) with a 32-byte key from config.
|
||||
*
|
||||
* Format of seal(): base64( iv[12] || tag[16] || ciphertext ), prefixed with
|
||||
* "enc:v1:" so we can tell encrypted values from any legacy plaintext and can
|
||||
* evolve the scheme later.
|
||||
*/
|
||||
class Secret
|
||||
{
|
||||
private const PREFIX = 'enc:v1:';
|
||||
private const CIPHER = 'aes-256-gcm';
|
||||
|
||||
/**
|
||||
* Resolve the 32-byte raw key from the configured 64-hex-char string.
|
||||
* Returns null when no (valid) key is configured.
|
||||
*/
|
||||
private static function key(?string $configKey): ?string
|
||||
{
|
||||
if (!$configKey) return null;
|
||||
$configKey = trim($configKey);
|
||||
// Accept a 64-char hex string (preferred) or a raw 32-byte key.
|
||||
if (preg_match('/^[0-9a-fA-F]{64}$/', $configKey)) {
|
||||
return hex2bin($configKey);
|
||||
}
|
||||
if (strlen($configKey) === 32) {
|
||||
return $configKey;
|
||||
}
|
||||
// Anything else is misconfigured; treat as "no key" so the caller can warn.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a token for storage. If no usable key is configured, returns the
|
||||
* plaintext unchanged and logs a warning (so dev still works, but production
|
||||
* never silently ships unencrypted tokens without a trace).
|
||||
*/
|
||||
public static function seal(string $plaintext, ?string $configKey): string
|
||||
{
|
||||
$key = self::key($configKey);
|
||||
if ($key === null) {
|
||||
if (function_exists('writeLog')) {
|
||||
writeLog('Secret::seal WARNING — no token_encryption_key set; storing token in PLAINTEXT.');
|
||||
}
|
||||
return $plaintext;
|
||||
}
|
||||
$iv = random_bytes(12);
|
||||
$tag = '';
|
||||
$ct = openssl_encrypt($plaintext, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv, $tag);
|
||||
if ($ct === false) {
|
||||
throw new RuntimeException('Secret::seal — encryption failed');
|
||||
}
|
||||
return self::PREFIX . base64_encode($iv . $tag . $ct);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt a stored token. Plaintext (un-prefixed) values are returned as-is
|
||||
* for backward compatibility with anything stored before a key was set.
|
||||
*/
|
||||
public static function open(string $stored, ?string $configKey): string
|
||||
{
|
||||
if (strncmp($stored, self::PREFIX, strlen(self::PREFIX)) !== 0) {
|
||||
return $stored; // legacy / plaintext
|
||||
}
|
||||
$key = self::key($configKey);
|
||||
if ($key === null) {
|
||||
throw new RuntimeException('Secret::open — value is encrypted but no key is configured');
|
||||
}
|
||||
$raw = base64_decode(substr($stored, strlen(self::PREFIX)), true);
|
||||
if ($raw === false || strlen($raw) < 28) {
|
||||
throw new RuntimeException('Secret::open — malformed ciphertext');
|
||||
}
|
||||
$iv = substr($raw, 0, 12);
|
||||
$tag = substr($raw, 12, 16);
|
||||
$ct = substr($raw, 28);
|
||||
$pt = openssl_decrypt($ct, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv, $tag);
|
||||
if ($pt === false) {
|
||||
throw new RuntimeException('Secret::open — decryption/auth failed (wrong key or tampered data)');
|
||||
}
|
||||
return $pt;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user