function old_key_candidates(string $k): array { // Try raw, trimmed, without wrapping quotes, and hex->bin $cands = []; $adds = []; $adds[] = $k; $adds[] = trim($k); $adds[] = trim($k, " \t\n\r\0\x0B'\""); // If it looks like hex, also try binary form foreach ($adds as $a) { $cands[] = $a; if (ctype_xdigit($a) && (strlen($a) % 2 === 0)) { $bin = @hex2bin($a); if ($bin !== false) $cands[] = $bin; } } // For each, also try md5(key) in both binary+hex (some v2 forks used SECRET) $all = []; foreach (array_unique($cands, SORT_STRING) as $cand) { $all[] = $cand; // raw $all[] = md5($cand, true); // md5 bin $all[] = md5($cand, false); // md5 hex } // de-duplicate by string identity return array_values(array_unique($all, SORT_STRING)); } // --- v2.x decrypt that tries many realistic permutations -------------------- function try_decrypt_v22(string $value, array $keys): ?string { $cipher = 'AES-256-CBC'; $ivlen = openssl_cipher_iv_length($cipher); $zeroIv = str_repeat("\0", $ivlen); $decoded = base64_decode($value, true); // default v2 produced base64 foreach ($keys as $key) { // 1) Exact legacy style — PHP auto-base64, empty IV param (omitted) $pt = @openssl_decrypt($value, $cipher, $key); if ($pt !== false) return $pt; // 2) base64-decoded + RAW_DATA + zero IV (some stacks behaved like this) if ($decoded !== false) { $pt = @openssl_decrypt($decoded, $cipher, $key, OPENSSL_RAW_DATA, $zeroIv); if ($pt !== false) return $pt; } // 3) explicit empty IV with base64 input (older PHP tolerated empty IV) $pt = @openssl_decrypt($value, $cipher, $key, 0, ''); if ($pt !== false) return $pt; if ($decoded !== false) { // 4) raw + empty IV, no RAW_DATA $pt = @openssl_decrypt($decoded, $cipher, $key, 0, ''); if ($pt !== false) return $pt; } } return null; } function decrypt_v31_with_key(string $value, string $keyBin): ?string { $decoded = base64_decode($value, true); if ($decoded === false) return null; $cipher = 'AES-256-CBC'; $ivlen = openssl_cipher_iv_length($cipher); $hmacLen = 32; if (strlen($decoded) < $ivlen + $hmacLen) return null; $iv = substr($decoded, 0, $ivlen); $hmc = substr($decoded, $ivlen, $hmacLen); $ct = substr($decoded, $ivlen + $hmacLen); $calc = hash_hmac('sha256', $ct, $keyBin, true); if (!hash_equals($hmc, $calc)) return null; $pt = openssl_decrypt($ct, $cipher, $keyBin, OPENSSL_RAW_DATA, $iv); return ($pt !== false) ? $pt : null; } function encrypt_v31_with_key(string $plaintext, string $keyBin): string { $cipher = 'AES-256-CBC'; $ivlen = openssl_cipher_iv_length($cipher); $iv = random_bytes($ivlen); $ct = openssl_encrypt($plaintext, $cipher, $keyBin, OPENSSL_RAW_DATA, $iv); $h = hash_hmac('sha256', $ct, $keyBin, true); return base64_encode($iv . $h . $ct); } // Re-key all rows with encrypt='1' function migrate_encrypted_pastes(PDO $pdo, string $oldKeyInput, string $newKeyHex, bool $dryRun=false): array { $res = ['checked'=>0,'converted'=>0,'skipped'=>0,'failed'=>0,'errors'=>[]]; if (!extension_loaded('openssl')) { $res['errors'][] = 'OpenSSL extension not available.'; return $res; } $oldKeyCandidates = old_key_candidates($oldKeyInput); if (empty($oldKeyCandidates)) { $res['errors'][] = 'Old $sec_key not provided.'; return $res; } $newKeyBin = @hex2bin(trim($newKeyHex)); if ($newKeyBin === false || strlen($newKeyBin)!==32) { $res['errors'][] = 'New $sec_key is not valid 64-hex (32-byte) key.'; return $res; } $total = (int)$pdo->query("SELECT COUNT(*) FROM pastes WHERE encrypt='1'")->fetchColumn(); if ($total === 0) return $res; $batch = 500; for ($offset=0; $offset<$total; $offset+=$batch) { $q = $pdo->prepare("SELECT id, content FROM pastes WHERE encrypt='1' ORDER BY id ASC LIMIT :lim OFFSET :off"); $q->bindValue(':lim', $batch, PDO::PARAM_INT); $q->bindValue(':off', $offset, PDO::PARAM_INT); $q->execute(); $rows = $q->fetchAll(PDO::FETCH_ASSOC); if (!$rows) break; if (!$dryRun) $pdo->beginTransaction(); foreach ($rows as $r) { $res['checked']++; $id = (int)$r['id']; $enc = (string)$r['content']; // Already new-format under current key? Skip. $already = decrypt_v31_with_key($enc, $newKeyBin); if ($already !== null) { $res['skipped']++; continue; } // Try legacy decrypt with multiple key interpretations $plain = try_decrypt_v22($enc, $oldKeyCandidates); if ($plain === null) { $res['failed']++; $res['errors'][] = "ID $id: could not decrypt with any old key variant."; continue; } // Re-encrypt and verify $reb = encrypt_v31_with_key($plain, $newKeyBin); if (decrypt_v31_with_key($reb, $newKeyBin) !== $plain) { $res['failed']++; $res['errors'][] = "ID $id: verification after re-encrypt failed."; continue; } if (!$dryRun) { $u = $pdo->prepare("UPDATE pastes SET content=:c WHERE id=:id"); $u->execute([':c'=>$reb, ':id'=>$id]); } $res['converted']++; } if (!$dryRun) $pdo->commit(); } return $res; }