- 1function old_key_candidates(string $k): array {
- 2 // Try raw, trimmed, without wrapping quotes, and hex->bin
- 3 $cands = [];
- 4 $adds = [];
- 5
- 6 $adds[] = $k;
- 7 $adds[] = trim($k);
- 8 $adds[] = trim($k, " \t\n\r\0\x0B'\"");
- 9
- 10 // If it looks like hex, also try binary form
- 11 foreach ($adds as $a) {
- 12 $cands[] = $a;
- 13 if (ctype_xdigit($a) && (strlen($a) % 2 === 0)) {
- 14 $bin = @hex2bin($a);
- 15 if ($bin !== false) $cands[] = $bin;
- 16 }
- 17 }
- 18
- 19 // For each, also try md5(key) in both binary+hex (some v2 forks used SECRET)
- 20 $all = [];
- 21 foreach (array_unique($cands, SORT_STRING) as $cand) {
- 22 $all[] = $cand; // raw
- 23 $all[] = md5($cand, true); // md5 bin
- 24 $all[] = md5($cand, false); // md5 hex
- 25 }
- 26
- 27 // de-duplicate by string identity
- 28 return array_values(array_unique($all, SORT_STRING));
- 29}
- 30
- 31// --- v2.x decrypt that tries many realistic permutations --------------------
- 32function try_decrypt_v22(string $value, array $keys): ?string {
- 33 $cipher = 'AES-256-CBC';
- 34 $ivlen = openssl_cipher_iv_length($cipher);
- 35 $zeroIv = str_repeat("\0", $ivlen);
- 36
- 37 $decoded = base64_decode($value, true); // default v2 produced base64
- 38
- 39 foreach ($keys as $key) {
- 40 // 1) Exact legacy style — PHP auto-base64, empty IV param (omitted)
- 41 $pt = @openssl_decrypt($value, $cipher, $key);
- 42 if ($pt !== false) return $pt;
- 43
- 44 // 2) base64-decoded + RAW_DATA + zero IV (some stacks behaved like this)
- 45 if ($decoded !== false) {
- 46 $pt = @openssl_decrypt($decoded, $cipher, $key, OPENSSL_RAW_DATA, $zeroIv);
- 47 if ($pt !== false) return $pt;
- 48 }
- 49
- 50 // 3) explicit empty IV with base64 input (older PHP tolerated empty IV)
- 51 $pt = @openssl_decrypt($value, $cipher, $key, 0, '');
- 52 if ($pt !== false) return $pt;
- 53
- 54 if ($decoded !== false) {
- 55 // 4) raw + empty IV, no RAW_DATA
- 56 $pt = @openssl_decrypt($decoded, $cipher, $key, 0, '');
- 57 if ($pt !== false) return $pt;
- 58 }
- 59 }
- 60 return null;
- 61}
- 62
- 63function decrypt_v31_with_key(string $value, string $keyBin): ?string {
- 64 $decoded = base64_decode($value, true);
- 65 if ($decoded === false) return null;
- 66 $cipher = 'AES-256-CBC';
- 67 $ivlen = openssl_cipher_iv_length($cipher);
- 68 $hmacLen = 32;
- 69 if (strlen($decoded) < $ivlen + $hmacLen) return null;
- 70 $iv = substr($decoded, 0, $ivlen);
- 71 $hmc = substr($decoded, $ivlen, $hmacLen);
- 72 $ct = substr($decoded, $ivlen + $hmacLen);
- 73 $calc = hash_hmac('sha256', $ct, $keyBin, true);
- 74 if (!hash_equals($hmc, $calc)) return null;
- 75 $pt = openssl_decrypt($ct, $cipher, $keyBin, OPENSSL_RAW_DATA, $iv);
- 76 return ($pt !== false) ? $pt : null;
- 77}
- 78
- 79function encrypt_v31_with_key(string $plaintext, string $keyBin): string {
- 80 $cipher = 'AES-256-CBC';
- 81 $ivlen = openssl_cipher_iv_length($cipher);
- 82 $iv = random_bytes($ivlen);
- 83 $ct = openssl_encrypt($plaintext, $cipher, $keyBin, OPENSSL_RAW_DATA, $iv);
- 84 $h = hash_hmac('sha256', $ct, $keyBin, true);
- 85 return base64_encode($iv . $h . $ct);
- 86}
- 87
- 88// Re-key all rows with encrypt='1'
- 89function migrate_encrypted_pastes(PDO $pdo, string $oldKeyInput, string $newKeyHex, bool $dryRun=false): array {
- 90 $res = ['checked'=>0,'converted'=>0,'skipped'=>0,'failed'=>0,'errors'=>[]];
- 91
- 92 if (!extension_loaded('openssl')) {
- 93 $res['errors'][] = 'OpenSSL extension not available.';
- 94 return $res;
- 95 }
- 96 $oldKeyCandidates = old_key_candidates($oldKeyInput);
- 97 if (empty($oldKeyCandidates)) {
- 98 $res['errors'][] = 'Old $sec_key not provided.';
- 99 return $res;
- 100 }
- 101 $newKeyBin = @hex2bin(trim($newKeyHex));
- 102 if ($newKeyBin === false || strlen($newKeyBin)!==32) {
- 103 $res['errors'][] = 'New $sec_key is not valid 64-hex (32-byte) key.';
- 104 return $res;
- 105 }
- 106
- 107 $total = (int)$pdo->query("SELECT COUNT(*) FROM pastes WHERE encrypt='1'")->fetchColumn();
- 108 if ($total === 0) return $res;
- 109
- 110 $batch = 500;
- 111 for ($offset=0; $offset<$total; $offset+=$batch) {
- 112 $q = $pdo->prepare("SELECT id, content FROM pastes WHERE encrypt='1' ORDER BY id ASC LIMIT :lim OFFSET :off");
- 113 $q->bindValue(':lim', $batch, PDO::PARAM_INT);
- 114 $q->bindValue(':off', $offset, PDO::PARAM_INT);
- 115 $q->execute();
- 116 $rows = $q->fetchAll(PDO::FETCH_ASSOC);
- 117 if (!$rows) break;
- 118
- 119 if (!$dryRun) $pdo->beginTransaction();
- 120 foreach ($rows as $r) {
- 121 $res['checked']++;
- 122 $id = (int)$r['id'];
- 123 $enc = (string)$r['content'];
- 124
- 125 // Already new-format under current key? Skip.
- 126 $already = decrypt_v31_with_key($enc, $newKeyBin);
- 127 if ($already !== null) { $res['skipped']++; continue; }
- 128
- 129 // Try legacy decrypt with multiple key interpretations
- 130 $plain = try_decrypt_v22($enc, $oldKeyCandidates);
- 131 if ($plain === null) {
- 132 $res['failed']++;
- 133 $res['errors'][] = "ID $id: could not decrypt with any old key variant.";
- 134 continue;
- 135 }
- 136
- 137 // Re-encrypt and verify
- 138 $reb = encrypt_v31_with_key($plain, $newKeyBin);
- 139 if (decrypt_v31_with_key($reb, $newKeyBin) !== $plain) {
- 140 $res['failed']++;
- 141 $res['errors'][] = "ID $id: verification after re-encrypt failed.";
- 142 continue;
- 143 }
- 144
- 145 if (!$dryRun) {
- 146 $u = $pdo->prepare("UPDATE pastes SET content=:c WHERE id=:id");
- 147 $u->execute([':c'=>$reb, ':id'=>$id]);
- 148 }
- 149 $res['converted']++;
- 150 }
- 151 if (!$dryRun) $pdo->commit();
- 152 }
- 153
- 154 return $res;
- 155}
Raw Paste