function old_key_candidates
(string
$k): array {
// Try raw, trimmed, without wrapping quotes, and hex->bin
$cands = [];
$adds = [];
$adds[] = $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 ($bin !== false) $cands[] = $bin;
}
}
// For each, also try md5(key) in both binary+hex (some v2 forks used SECRET)
$all = [];
$all[] = $cand; // raw
$all[] = md5($cand, true); // md5 bin
$all[] = md5($cand, false); // md5 hex
}
// de-duplicate by string identity
}
// --- v2.x decrypt that tries many realistic permutations --------------------
function try_decrypt_v22
(string
$value, array $keys): ?string
{
$cipher = 'AES-256-CBC';
$decoded = base64_decode($value, true); // default v2 produced base64
foreach ($keys as $key) {
// 1) Exact legacy style — PHP auto-base64, empty IV param (omitted)
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)
if ($pt !== false) return $pt;
if ($decoded !== false) {
// 4) raw + empty IV, no RAW_DATA
if ($pt !== false) return $pt;
}
}
return null;
}
function decrypt_v31_with_key(string $value, string $keyBin): ?string {
if ($decoded === false) return null;
$cipher = 'AES-256-CBC';
$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);
return ($pt !== false) ? $pt : null;
}
function encrypt_v31_with_key(string $plaintext, string $keyBin): string {
$cipher = 'AES-256-CBC';
$ct = openssl_encrypt($plaintext, $cipher, $keyBin, OPENSSL_RAW_DATA
, $iv);
$h = hash_hmac('sha256', $ct, $keyBin, true);
}
// 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'=>[]];
$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;
}
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;
}