<?php if(!defined('PRIVATE_DIR'))exit; class SecureVault{ private static ?string $master=null; private static array $keyCache=[]; private static string $ctx='cleanpii'; private static array $domainMap=['default'=>0,'user'=>1,'order'=>2,'review'=>3,'log'=>4,'payment'=>5,'faq'=>6,'product'=>7]; private static array $idMap=[0=>'default',1=>'user',2=>'order',3=>'review',4=>'log',5=>'payment',6=>'faq',7=>'product']; private static function masterKey():string{ if(self::$master!==null)return self::$master; require_once __DIR__ . '/credentials.php'; $hex = CredentialStore::get('pii_key'); if($hex && strlen($hex)===64 && ctype_xdigit($hex)){ self::$master=sodium_hex2bin($hex); return self::$master; } if(defined('ENCRYPTION_KEY')&&ENCRYPTION_KEY!==''){ $hex=ENCRYPTION_KEY; if(strlen($hex)===64&&ctype_xdigit($hex)){ self::$master=sodium_hex2bin($hex); return self::$master; } } throw new RuntimeException('Encryption key not found'); } private static function domainKey(int $subkeyId):string{ if(isset(self::$keyCache[$subkeyId]))return self::$keyCache[$subkeyId]; $master=self::masterKey(); if($subkeyId===0){self::$keyCache[0]=$master;return $master;} $k=sodium_crypto_kdf_derive_from_key(SODIUM_CRYPTO_SECRETBOX_KEYBYTES,$subkeyId,self::$ctx,$master); self::$keyCache[$subkeyId]=$k; return $k; } public static function encrypt(string $plaintext,string|int $domain='default'):string{ $subkeyId=is_string($domain)?(self::$domainMap[$domain]??0):(int)$domain; $key=self::domainKey($subkeyId); $nonce=random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); $tag=chr($subkeyId); return base64_encode($tag.$nonce.sodium_crypto_secretbox($plaintext,$nonce,$key)); } public static function decrypt(string $encoded):string{ if($encoded==='')return ''; $decoded=base64_decode($encoded,true); if($decoded===false)throw new RuntimeException('Invalid base64 encoding'); $minTagged=1+SODIUM_CRYPTO_SECRETBOX_NONCEBYTES+SODIUM_CRYPTO_SECRETBOX_MACBYTES; if(strlen($decoded)>=$minTagged){ $tag=ord($decoded[0]); if($tag<=7){ $nonce=substr($decoded,1,SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); $cipher=substr($decoded,1+SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); try{$key=self::domainKey($tag);$plain=sodium_crypto_secretbox_open($cipher,$nonce,$key);if($plain!==false)return $plain;}catch(\Throwable $e){throw new RuntimeException('Decryption failed: ' . $e->getMessage());} } } throw new RuntimeException('Decryption failed: invalid format or tag'); } public static function encryptFields(array $data,array $fields,string $domain='default'):array{ foreach($fields as $field){if(isset($data[$field])&&$data[$field]!==''){$data[$field]=self::encrypt($data[$field],$domain);}} return $data; } public static function decryptFields(array $data,array $fields):array{ foreach($fields as $field){if(isset($data[$field])&&$data[$field]!==''){try{$data[$field]=self::decrypt($data[$field]);}catch(RuntimeException $e){$data[$field]='[DECRYPT_ERROR]';}}} return $data; } public static function isEncrypted(string $value):bool{ $minDecoded=1+SODIUM_CRYPTO_SECRETBOX_NONCEBYTES+SODIUM_CRYPTO_SECRETBOX_MACBYTES; if(strlen($value)<(int)ceil($minDecoded*4/3))return false; $decoded=base64_decode($value,true); if($decoded===false||strlen($decoded)<$minDecoded)return false; return ord($decoded[0])<=7; } public static function hash(string $value):string{return hash('sha256',strtolower(trim($value)));} public static function render(string $value):string{if($value==='')return '';try{return self::decrypt($value);}catch(\Throwable $e){return $value;}} public static function d(string $value):string{if($value==='')return '';try{return self::decrypt($value);}catch(\Throwable $e){return $value;}} public static function e(string $value):string{if($value==='')return '';try{return htmlspecialchars(self::decrypt($value),ENT_QUOTES,'UTF-8');}catch(\Throwable $e){return htmlspecialchars($value,ENT_QUOTES,'UTF-8');}} public static function rows(array $rows,array $columns):array{foreach($rows as &$r){self::decryptFields($r,$columns);}unset($r);return $rows;} public static function domainId(string $name):int{return self::$domainMap[$name]??0;} public static function domainName(int $id):string{return self::$idMap[$id]??'default';} const DOMAIN_DEFAULT=0; const DOMAIN_USER=1; const DOMAIN_ORDER=2; const DOMAIN_REVIEW=3; const DOMAIN_LOG=4; const DOMAIN_PAYMENT=5; const DOMAIN_FAQ=6; const DOMAIN_PRODUCT=7; } class_alias('SecureVault','Encryption'); class_alias('SecureVault','Crypt'); 