2-bo‘lim

Baytlar, kodlash va xavfsiz solishtirish

Kriptografiya matn bilan emas, baytlar bilan ishlaydi - va ularni solishtirish alohida ehtiyot talab qiladi.

🕑 16 daqiqa o‘qish 📄 1 030 so‘z 👁 1 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Bayt va belgi bir xil emas
  2. Nima uchun hex va base64 kerak
  3. URL uchun base64
  4. Endi eng muhim qism: solishtirish
  5. Yechim: hash_equals
  6. == va tur o'yinlari
  7. hash_equals qanday ishlaydi
  8. Xavfsiz token yaratish
  9. Xulosa

Kriptografik funksiyalar matn emas, baytlar qaytaradi. Bu farqni tushunmaslik ko'p xatoga sabab bo'ladi.

Bu bo'limda baytlar bilan ishlash va ularni xavfsiz solishtirish bilan tanishamiz.

Bayt va belgi bir xil emas #

PHP
function korsat(string $nom, string $qiymat): void
{
    printf("%-14s %s%s", $nom . ':', $qiymat, PHP_EOL);
}
PHP
$lotin = 'Salom';
$kirill = 'Салом';
$emoji = 'Salom 👋';

foreach (['lotin' => $lotin, 'kirill' => $kirill, 'emoji' => $emoji] as $n => $s) {
    printf("%-8s strlen=%-3d mb_strlen=%-3d%s", $n,
        strlen($s), mb_strlen($s, 'UTF-8'), PHP_EOL);
}
Natija
lotin    strlen=5   mb_strlen=5  
kirill   strlen=10  mb_strlen=5  
emoji    strlen=10  mb_strlen=7  
Kriptografiyada doim strlen kerak

strlen baytlarni sanaydi, mb_strlen esa belgilarni.

Kriptografik funksiyalar bayt bilan ishlaydi:

TalabTo'g'ri o'lchov
AES-256 kaliti32 bayt
GCM nonce12 bayt
SHA-256 natijasi32 bayt

Agar kalitni foydalanuvchi kiritgan matndan olsangiz, mb_strlen bilan tekshirish xato: "Салом" beshta belgi, lekin o'n bayt.

PHP satrlari ikkilik xavfsiz (binary safe) - ular ichida nol bayt (\x00) ham bo'lishi mumkin va strlen uni to'g'ri sanaydi.

PHP
$baytlar = "\x00\x01\xff\x00";

echo 'uzunlik: ', strlen($baytlar), PHP_EOL;
echo 'hex:     ', bin2hex($baytlar), PHP_EOL;
echo 'nol bayt bormi: ',
    str_contains($baytlar, "\x00") ? 'ha' : 'yoq', PHP_EOL;
Natija
uzunlik: 4
hex:     0001ff00
nol bayt bormi: ha

Nima uchun hex va base64 kerak #

PHP
$xesh = hash('sha256', 'parol123', true);   // true = xom baytlar

echo 'xom baytlar uzunligi: ', strlen($xesh), PHP_EOL;
echo 'hex:                  ', bin2hex($xesh), PHP_EOL;
echo 'hex uzunligi:         ', strlen(bin2hex($xesh)), PHP_EOL;
echo 'base64:               ', base64_encode($xesh), PHP_EOL;
echo 'base64 uzunligi:      ', strlen(base64_encode($xesh)), PHP_EOL;
Natija
xom baytlar uzunligi: 32
hex:                  a203ba4e9c565ec07ecedc94a54194aa0b237cf65203aca0e1bd420c93f2cc28
hex uzunligi:         64
base64:               ogO6TpxWXsB+ztyUpUGUqgsjfPZSA6yg4b1CDJPyzCg=
base64 uzunligi:      44
Uch format, uch maqsad
FormatUzunlik (32 bayt uchun)Qachon
Xom baytlar32Xotirada, ichki hisob-kitob
Hex64Jurnal, ko'z bilan solishtirish
Base6444JSON, HTTP sarlavha, URL

Base64 hex dan qisqaroq, lekin hex o'qish uchun qulayroq.

Diqqat: hash() funksiyasining uchinchi argumenti false bo'lsa (standart holat), u hex satr qaytaradi. Ko'p kodda bu chalkashlik keltirib chiqaradi - strlen 64 beradi, 32 emas.

PHP
// Uchinchi argument farqi
$hexShakl = hash('sha256', 'test');
$xomShakl = hash('sha256', 'test', true);

echo 'standart (hex): ', strlen($hexShakl), ' bayt', PHP_EOL;
echo 'xom (binary):   ', strlen($xomShakl), ' bayt', PHP_EOL;
var_dump($hexShakl === bin2hex($xomShakl));
Natija
standart (hex): 64 bayt
xom (binary):   32 bayt
bool(true)

URL uchun base64 #

PHP
$baytlar = hex2bin('fbff3e7d0a');

$oddiy = base64_encode($baytlar);
echo 'oddiy base64: ', $oddiy, PHP_EOL;

// URL da +, / va = muammo tug'diradi
$url = rtrim(strtr($oddiy, '+/', '-_'), '=');
echo 'base64url:    ', $url, PHP_EOL;

// qaytarish
$qayta = base64_decode(strtr($url, '-_', '+/'));
var_dump(bin2hex($qayta));
Natija
oddiy base64: +/8+fQo=
base64url:    -_8-fQo
string(10) "fbff3e7d0a"

Endi eng muhim qism: solishtirish #

PHP
// FAQAT NAMOYISH UCHUN: nechta bayt solishtirilganini sanaydi
function sanovchiSolishtir(string $a, string $b, ?int &$solishtirildi): bool
{
    $solishtirildi = 0;

    if (strlen($a) !== strlen($b)) {
        return false;
    }

    for ($i = 0, $n = strlen($a); $i < $n; $i++) {
        $solishtirildi++;
        if ($a[$i] !== $b[$i]) {
            return false;      // BIRINCHI farqda to'xtaydi
        }
    }

    return true;
}
PHP
$togri = 'a1b2c3d4e5f6a7b8';

$urinishlar = [
    'xxxxxxxxxxxxxxxx',   // birinchi bayt xato
    'a1xxxxxxxxxxxxxx',   // ikkinchi bayt xato
    'a1b2c3d4xxxxxxxx',   // yarmi to'g'ri
    'a1b2c3d4e5f6a7b8',   // to'liq to'g'ri
];

foreach ($urinishlar as $u) {
    sanovchiSolishtir($togri, $u, $n);
    printf('%-18s %2d bayt solishtirildi%s', substr($u, 0, 10) . '...',
        $n, PHP_EOL);
}
Natija
xxxxxxxxxx...       1 bayt solishtirildi
a1xxxxxxxx...       3 bayt solishtirildi
a1b2c3d4xx...       9 bayt solishtirildi
a1b2c3d4e5...      16 bayt solishtirildi
Solishtirilgan baytlar soni - oshkor bo'lgan ma'lumot

Yuqoridagi jadvalga qarang: qancha ko'p bayt to'g'ri bo'lsa, funksiya shuncha uzoq ishlaydi.

Real hujumda hujumchi baytlar sonini ko'rmaydi, lekin vaqtni o'lchay oladi. Farq juda kichik (nanosekundlar), lekin:

SharoitNatija
Bir marta o'lchashShovqin ichida yo'qoladi
Minglab marta o'lchash va o'rtacha olishFarq ko'rinadi

Shunday qilib hujumchi tokenni bayt-bayt topib boradi: avval birinchi baytni, keyin ikkinchisini. 16 baytlik token uchun 256×16 = 4096 urinish yetadi - 256¹⁶ o'rniga.

Bu vaqt bo'yicha yon kanal (timing side channel) hujumi.

Yechim: hash_equals #

PHP
$togri = 'a1b2c3d4e5f6a7b8';

// hash_equals HAR DOIM barcha baytni solishtiradi
var_dump(hash_equals($togri, 'xxxxxxxxxxxxxxxx'));
var_dump(hash_equals($togri, 'a1b2c3d4xxxxxxxx'));
var_dump(hash_equals($togri, 'a1b2c3d4e5f6a7b8'));

// uzunlik farq qilsa - darhol false
var_dump(hash_equals($togri, 'qisqa'));
Natija
bool(false)
bool(false)
bool(true)
bool(false)
hash_equals qachon ishlatiladi
Solishtirilayotgan narsaFunksiya
HMAC yoki imzohash_equals
API kaliti, tokenhash_equals
Sessiya identifikatorihash_equals
Parolni tiklash kodihash_equals
CSRF tokenihash_equals
Parol xeshipassword_verify (o'zi doimiy vaqtda)
Oddiy matn (foydalanuvchi ismi)=== yetarli

Qoida oddiy: maxfiy qiymat solishtirilayotgan bo'lsa, hash_equals ishlating.

Nomi chalkash: funksiya faqat xesh uchun emas, istalgan ikki satr uchun ishlaydi.

Diqqat: uzunliklar farq qilsa u darhol false qaytaradi. Ya'ni uzunlik yashirilmaydi - lekin bu odatda muammo emas, chunki token uzunligi ochiq ma'lumot.

== va tur o'yinlari #

PHP da == satrlarni ba'zan son sifatida solishtiradi:

PHP
// Ikkalasining ham MD5 xeshi "0e" bilan boshlanadi
$a = '240610708';
$b = 'QNKCDZO';

echo 'md5(a): ', md5($a), PHP_EOL;
echo 'md5(b): ', md5($b), PHP_EOL;

var_dump(md5($a) == md5($b));    // == bilan
var_dump(md5($a) === md5($b));   // === bilan
Natija
md5(a): 0e462097431906509019562988736854
md5(b): 0e830400451993494058024219903391
bool(true)
bool(false)
Bu haqiqiy zaiflik - "sehrli xesh"

0e462097... va 0e830400... - ikkalasi ham 0e bilan boshlanib, qolgani faqat raqamlardan iborat.

PHP 7 va undan oldingi versiyalarda == bunday satrlarni ilmiy yozuvdagi son deb qabul qilardi: 0e462097... = 0 × 10⁴⁶²⁰⁹⁷ = 0. Ikkala tomon ham nolga teng bo'lib chiqardi.

PHP 8 da == ning xulqi o'zgardi: satr va satr solishtirilganda ular satr sifatida qiyoslanadi. Lekin:

HolatPHP 8 da natija
'0e123' == '0e456'Ikkalasi ham raqamli satr - true
'abc' == 0false (PHP 8 da tuzatilgan)
md5($a) == md5($b)Yuqoridagi natijaga qarang

Xulosa o'zgarmaydi: maxfiy qiymatlarni hech qachon == bilan solishtirmang. hash_equals yoki hech bo'lmasa === ishlating.

Vaqt bo'yicha yon kanal va undan himoya Erta to'xtaydigan solishtirish if ($a[$i] !== $b[$i]) return false; xxxx... 1 bayt a1xx... 2 bayt a1b2c3d4... 9 bayt a1b2c3d4e5f6a7b8 16 bayt Vaqt to'g'ri baytlar soniga bog'liq → token bayt-bayt topiladi hash_equals $farq |= $a[$i] ^ $b[$i]; xxxx... a1xx... a1b2c3d4... a1b2c3d4e5f6a7b8 har doim 16 bayt Vaqt kiritmaga bog'liq emas → hech qanday ma'lumot oshkor bo'lmaydi Doimiy vaqt tamoyili Maxfiy ma'lumot ustidagi amal qancha vaqt olishi shu ma'lumotga bog'liq bo'lmasin. Bu shart faqat solishtirishga emas, tarmoq javoblari va xato xabarlariga ham tegishli.
Erta to'xtash qulay, lekin maxfiy qiymatlar uchun xavfli

hash_equals qanday ishlaydi #

PHP
// Tamoyilni ko'rsatuvchi soddalashtirilgan versiya
function doimiyVaqtSolishtir(string $a, string $b): bool
{
    if (strlen($a) !== strlen($b)) {
        return false;
    }

    $farq = 0;
    for ($i = 0, $n = strlen($a); $i < $n; $i++) {
        // XOR: bir xil baytlar 0 beradi, farqli baytlar 0 dan katta
        $farq |= ord($a[$i]) ^ ord($b[$i]);
    }

    // hech qayerda erta return yo'q
    return $farq === 0;
}

$togri = 'a1b2c3d4e5f6a7b8';

var_dump(doimiyVaqtSolishtir($togri, 'a1b2c3d4e5f6a7b8'));
var_dump(doimiyVaqtSolishtir($togri, 'a1b2c3d4e5f6a7b9'));
var_dump(doimiyVaqtSolishtir($togri, 'xxxxxxxxxxxxxxxx'));
var_dump(doimiyVaqtSolishtir($togri, 'qisqa'));
Natija
bool(true)
bool(false)
bool(false)
bool(false)
Bu funksiyani ishlatmang - hash_equals ni ishlating

Yuqoridagi kod tamoyilni ko'rsatish uchun yozilgan.

Real kodda hash_equals afzal, chunki:

SababIzoh
C darajasida yozilganPHP darajasidagi optimallashtirishlar aralashmaydi
Kompilyator uni "soddalashtirmaydi"PHP kodini JIT o'zgartirishi mumkin
Sinovdan o'tganYillar davomida tekshirilgan
QisqaroqBir qator

Bu 1-bo'limdagi qoidaning takrori: primitivni o'zingiz yozmang.

Xavfsiz token yaratish #

PHP
// Token uzunligi - baytlarda o'lchanadi
$tokenBaytlari = random_bytes(32);

echo 'baytlar:   ', strlen($tokenBaytlari), PHP_EOL;
echo 'hex:       ', strlen(bin2hex($tokenBaytlari)), ' belgi', PHP_EOL;
echo 'base64:    ', strlen(base64_encode($tokenBaytlari)), ' belgi', PHP_EOL;
echo 'base64url: ', strlen(rtrim(strtr(
    base64_encode($tokenBaytlari), '+/', '-_'), '=')), ' belgi', PHP_EOL;

// Har chaqiruv boshqa natija beradi
$a = bin2hex(random_bytes(16));
$b = bin2hex(random_bytes(16));
var_dump($a === $b);
Natija
baytlar:   32
hex:       64 belgi
base64:    44 belgi
base64url: 43 belgi
bool(false)
rand() va mt_rand() token uchun yaramaydi
FunksiyaKriptografikQachon
rand(), mt_rand()Yo'qO'yin, simulyatsiya
uniqid()Yo'qHech qachon (vaqtga asoslangan)
shuffle(), array_rand()Yo'qKo'rgazma
random_bytes()HaToken, kalit, nonce
random_int()HaTasodifiy son

mt_rand Mersenne Twister generatoridan foydalanadi. Uning ichki holatini bir necha chiqishdan tiklash mumkin - ya'ni hujumchi keyingi tokenlarni oldindan hisoblab qo'yadi.

uniqid() esa mikrosekundlardagi vaqtga asoslangan - uni taxmin qilish yanada oson.

Bu mavzu 15-bo'limda batafsil ko'riladi.

Amaliy topshiriq
  1. Lotin, kirill va emoji satrlar uchun strlen va mb_strlen ni solishtiring.
  2. Nol bayt ichidagi satr yarating va uzunligini tekshiring.
  3. SHA-256 ni xom va hex shaklda oling, uzunliklarini yozing.
  4. Bir xil baytlarni hex, base64 va base64url da chiqaring.
  5. Base64url ni qayta oddiy base64 ga aylantirib, dastlabki baytlarni oling.
  6. Sanovchi solishtiruvchi yozing va turli kiritmalar uchun bayt sonini o'lchang.
  7. Nima uchun bu son hujumchiga foydali ekanini tushuntiring.
  8. hash_equals bilan xuddi shu solishtirishlarni bajaring.
  9. md5('240610708') == md5('QNKCDZO') natijasini o'z PHP versiyangizda tekshiring.
  10. random_bytes va mt_rand farqini uch qatorda yozing.

Xulosa #

  • Kriptografiya baytlar bilan ishlaydi - strlen, mb_strlen emas.
  • PHP satrlari ikkilik xavfsiz: ular nol bayt saqlashi mumkin.
  • hash() standart holda hex satr, uchinchi argument true bo'lsa xom baytlar qaytaradi.
  • Hex - o'qish uchun, base64 - uzatish uchun, xom baytlar - hisob-kitob uchun.
  • URL uchun +/ belgilari -_ ga almashtiriladi (base64url).
  • Erta to'xtaydigan solishtirish vaqt orqali ma'lumot oshkor qiladi.
  • Bu hujum tokenni bayt-bayt topishga imkon beradi: 256×16 urinish, 256¹⁶ o'rniga.
  • Maxfiy qiymatlar uchun har doim hash_equals ishlating.
  • == PHP da tur o'yinlariga sabab bo'ladi - kamida === ishlating.
  • Token va kalitlar faqat random_bytes / random_int bilan yaratiladi.

Keyingi bo'limda xesh funksiyalari bilan tanishamiz.

Xatolik topdingizmi?

Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.