16-bo‘lim

Ma'lumotlarni saqlashda shifrlash

Bazadagi maydonni shifrlash - konvert sxemasi, kalit almashtirish va shifrlangan maydon bo'yicha qidirish.

🕑 13 daqiqa o‘qish 📄 1 121 so‘z 👁 1 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Tahdid modeli
  2. Ilova darajasida shifrlash
  3. Shifrlangan maydon bo'yicha qidirish
  4. Yechim: ko'r indeks
  5. To'liq misol: shifrlangan jadval
  6. Konvert sxemasi va kalit almashtirish
  7. Nimalarni shifrlash kerak
  8. Xulosa

Bazani shifrlash haqida gapirishdan oldin bitta savolga javob berish kerak: kimdan himoyalanmoqdamiz?

Bu savolga javob bermasdan qilingan shifrlash ko'pincha foyda bermaydi.

Tahdid modeli #

TahdidDisk shifriUstun shifriIlova shifri
O'g'irlangan diskHimoyalaydiHimoyalaydiHimoyalaydi
Zaxira nusxa sizib chiqdiYo'qHimoyalaydiHimoyalaydi
SQL in'yeksiyasi orqali dumpYo'qYo'qHimoyalaydi
Baza administratoriYo'qQismanHimoyalaydi
Ilova serveri buzildiYo'qYo'qYo'q
Oxirgi qatorga diqqat

Agar hujumchi ilova serverini egallasa, u kalitlarga ham ega bo'ladi. Shifrlash bu holatda himoya bermaydi.

Bu shifrlash foydasiz degani emas - u boshqa uch tahdidni qoplaydi. Lekin uni "hamma narsadan himoya" deb hisoblash xato.

Ko'p tashkilot "biz ma'lumotni shifrlaymiz" deydi va disk shifrini nazarda tutadi. Disk shifri esa server o'chirilgan paytda ishlaydi. Server ishlab turganda disk allaqachon ochilgan - demak baza dumpi ochiq matnda chiqadi.

Ilova darajasida shifrlash #

PHP
function korsat(string $nom, string $qiymat): void
{
    printf("%-20s %s%s", $nom . ':', $qiymat, PHP_EOL);
}

final class MaydonShifri
{
    private const VERSIYA = 1;

    public function __construct(private readonly string $kalit)
    {
        if (strlen($kalit) !== 32) {
            throw new InvalidArgumentException('kalit 32 bayt bolishi kerak');
        }
    }

    public function shifrla(string $matn, string $kontekst): string
    {
        $nonce = random_bytes(12);
        $teg = '';

        $shifr = openssl_encrypt($matn, 'aes-256-gcm', $this->kalit,
            OPENSSL_RAW_DATA, $nonce, $teg, $kontekst);

        // Bazaga matn sifatida saqlanadigan shakl
        return sprintf('v%d.%s.%s.%s', self::VERSIYA,
            base64_encode($nonce), base64_encode($teg),
            base64_encode($shifr));
    }

    public function och(string $saqlangan, string $kontekst): string
    {
        $qismlar = explode('.', $saqlangan, 4);

        if (count($qismlar) !== 4 || $qismlar[0] !== 'v' . self::VERSIYA) {
            throw new RuntimeException('format notogri yoki versiya nomalum');
        }

        $matn = openssl_decrypt(base64_decode($qismlar[3]), 'aes-256-gcm',
            $this->kalit, OPENSSL_RAW_DATA,
            base64_decode($qismlar[1]), base64_decode($qismlar[2]),
            $kontekst);

        if ($matn === false) {
            throw new RuntimeException('butunlik tekshiruvi otmadi');
        }

        return $matn;
    }
}
PHP
$shifr = new MaydonShifri(hex2bin(str_repeat('4d', 32)));

// Kontekst - AAD: yozuv identifikatori va ustun nomi (8-bo'lim)
$kontekst = 'mijozlar:42:karta';
$saqlangan = $shifr->shifrla('8600123456781234', $kontekst);

// Qiymat har shifrlashda boshqacha - shakli va uzunligini ko'rsatamiz
$qismlar = explode('.', $saqlangan);
korsat('shakli', 'v1.<nonce>.<teg>.<shifrmatn>');
korsat('qismlar soni', count($qismlar) . ' ta');
korsat('versiya', $qismlar[0]);
korsat('uzunligi', strlen($saqlangan) . ' belgi');
korsat('ochilgan', $shifr->och($saqlangan, $kontekst));

// Boshqa yozuvga ko'chirishga urinish
try {
    $shifr->och($saqlangan, 'mijozlar:99:karta');
} catch (RuntimeException $x) {
    korsat('boshqa yozuvda', 'RAD: ' . $x->getMessage());
}
Natija
shakli:              v1.<nonce>.<teg>.<shifrmatn>
qismlar soni:        4 ta
versiya:             v1
uzunligi:            69 belgi
ochilgan:            8600123456781234
boshqa yozuvda:      RAD: butunlik tekshiruvi otmadi
Bazadagi ustun turi va hajmi

Shifrlangan qiymat asl matndan uzunroq:

QismHajmi
Versiya belgisi3 belgi (v1.)
Nonce (base64)16 belgi
Teg (base64)24 belgi
Shifrmatn (base64)asl hajm × 4/3
Nuqtalar3 belgi

16 baytlik karta raqami uchun jami ~70 belgi.

Amaliy tavsiyalar:

MasalaYechim
Ustun turiVARBINARY yoki TEXT
HajmAsl hajm × 1.4 + 50
IndeksTo'g'ridan-to'g'ri qo'yib bo'lmaydi
ORDER BYIshlamaydi
LIKE qidiruvIshlamaydi

Oxirgi uchtasi keyingi bo'limning mavzusi.

Shifrlangan maydon bo'yicha qidirish #

PHP
$shifr = new MaydonShifri(hex2bin(str_repeat('4d', 32)));

// Bir xil qiymat, ikki marta shifrlangan
$a = $shifr->shifrla('[email protected]', 'mijozlar:1:email');
$b = $shifr->shifrla('[email protected]', 'mijozlar:2:email');

korsat('1-yozuv uzunligi', strlen($a) . ' belgi');
korsat('2-yozuv uzunligi', strlen($b) . ' belgi');
korsat('bir xilmi', $a === $b ? 'ha' : 'YOQ');

echo PHP_EOL, "SELECT ... WHERE email = ? ishlamaydi:", PHP_EOL;
echo '  har shifrlashda nonce boshqa - qiymat ham boshqa', PHP_EOL;
Natija
1-yozuv uzunligi:    73 belgi
2-yozuv uzunligi:    73 belgi
bir xilmi:           YOQ

SELECT ... WHERE email = ? ishlamaydi:
  har shifrlashda nonce boshqa - qiymat ham boshqa
Bu muammo emas, xususiyat

Har shifrlashda boshqa natija chiqishi - to'g'ri xulq (7-bo'limdagi ECB muammosini eslang).

Agar shifrlash deterministik bo'lsa, ya'ni bir xil qiymat har doim bir xil shifrmatn bersa, qidiruv ishlaydi - lekin baza ma'lumot sizdiradi:

Deterministik shifrlashda ko'rinadiMisol
Qaysi yozuvlarda bir xil qiymat bor500 ta mijozda bir xil manzil
Har qiymat necha marta uchraydiChastota tahlili
Qiymatlar taqsimotiKam uchraydigan qiymatni topish

Masalan "kasallik" ustuni deterministik shifrlansa, hujumchi shifrmatnlarni sanab, eng ko'p uchraydigani qaysi kasallik ekanini taxmin qila oladi.

Shuning uchun deterministik shifrlash faqat juda cheklangan holatlarda ishlatiladi.

Yechim: ko'r indeks #

PHP
final class KorIndeks
{
    public function __construct(private readonly string $indeksKaliti) {}

    /**
     * Qidiruv uchun qiymatning HMAC i.
     * Shifrlash kalitidan ALOHIDA kalit ishlatiladi.
     */
    public function indeks(string $qiymat, string $ustun): string
    {
        // Normallashtirish - qidiruv izchil bo'lishi uchun
        $tayyor = mb_strtolower(trim($qiymat), 'UTF-8');

        $toliq = hash_hmac('sha256', $ustun . ':' . $tayyor,
            $this->indeksKaliti, true);

        // Qisqartirish ataylab: to'qnashuvlar chastota tahlilini qiyinlashtiradi
        return bin2hex(substr($toliq, 0, 8));
    }
}
PHP
$indeks = new KorIndeks(hex2bin(str_repeat('7e', 32)));

$emaillar = [
    '[email protected]',
    '[email protected]',    // katta harf
    '  [email protected] ', // ortiqcha bo'shliq
    '[email protected]',
];

foreach ($emaillar as $e) {
    printf('%-24s %s%s', "'" . trim($e) . "'",
        $indeks->indeks($e, 'email'), PHP_EOL);
}

echo PHP_EOL, 'birinchi uchtasi bir xil - normallashtirish ishladi', PHP_EOL;
Natija
'[email protected]'    9c4fc3266a446023
'[email protected]'    9c4fc3266a446023
'[email protected]'    9c4fc3266a446023
'[email protected]'      b6e5c6e08717ddfe

birinchi uchtasi bir xil - normallashtirish ishladi
Ko'r indeks - shifrlangan maydon bo'yicha qidirish Bazadagi jadval id email_shifr (AES-GCM, har safar boshqa) email_indeks (HMAC, doimiy) yaratilgan 42 v1.k3Nn.../8fA2... a91f4c02e8b7d3f1 2025-01-15 indeks qo'yib bo'lmaydi INDEX qo'yiladi - qidiruv tez Qidiruv qanday boradi 1. $i = HMAC(kalit, 'email:' . tozalangan) 2. WHERE email_indeks = $i 3. topilganlarni ochib, aniq solishtirish Nima baribir sizadi Bir xil qiymatli yozuvlar bir xil indeksga ega → "bu ikki mijozning emaili bir xil" Indeksni qisqartirish buni yumshatadi Nima ishlaydi, nima ishlamaydi ✓ Aniq moslik: WHERE email_indeks = ? ✓ Noyoblik cheklovi: UNIQUE(email_indeks) LIKE '%qism%' - qism bo'yicha qidiruv ORDER BY, >, < - tartiblash va oraliq Qism bo'yicha qidiruv kerak bo'lsa - shifrlash o'rniga boshqa himoya usulini o'ylang.
Ko'r indeks aniq moslikni beradi, qolgan so'rov turlarini emas

To'liq misol: shifrlangan jadval #

PHP
final class MijozOmbori
{
    /** @var array<int, array{email_shifr: string, email_indeks: string}> */
    private array $jadval = [];
    private int $keyingiId = 1;

    public function __construct(
        private readonly MaydonShifri $shifr,
        private readonly KorIndeks $indeks,
    ) {}

    public function qosh(string $email): int
    {
        $id = $this->keyingiId++;

        $this->jadval[$id] = [
            'email_shifr' => $this->shifr->shifrla($email, "mijozlar:{$id}:email"),
            'email_indeks' => $this->indeks->indeks($email, 'email'),
        ];

        return $id;
    }

    public function emailBoyichaTop(string $email): array
    {
        $qidiruv = $this->indeks->indeks($email, 'email');
        $topilgan = [];

        foreach ($this->jadval as $id => $qator) {
            // 1. Indeks bo'yicha filtr (bazada bu INDEX orqali tez ketadi)
            if (!hash_equals($qator['email_indeks'], $qidiruv)) {
                continue;
            }

            // 2. Ochib, ANIQ solishtirish - to'qnashuvga qarshi
            $ochilgan = $this->shifr->och($qator['email_shifr'],
                "mijozlar:{$id}:email");

            if (hash_equals($ochilgan, mb_strtolower(trim($email), 'UTF-8'))) {
                $topilgan[] = $id;
            }
        }

        return $topilgan;
    }

    public function soni(): int
    {
        return count($this->jadval);
    }
}
PHP
$ombor = new MijozOmbori(
    new MaydonShifri(hex2bin(str_repeat('4d', 32))),
    new KorIndeks(hex2bin(str_repeat('7e', 32))),
);

foreach (['[email protected]', '[email protected]',
          '[email protected]', '[email protected]'] as $e) {
    $ombor->qosh($e);
}

korsat('jadvalda', $ombor->soni() . ' ta yozuv');
korsat('malika qidiruvi',
    implode(', ', $ombor->emailBoyichaTop('[email protected]')));
korsat('katta harf bilan',
    implode(', ', $ombor->emailBoyichaTop('[email protected]')));
korsat('mavjud emas',
    $ombor->emailBoyichaTop('[email protected]') === []
        ? '(topilmadi)' : 'topildi');
Natija
jadvalda:            4 ta yozuv
malika qidiruvi:     2
katta harf bilan:    2
mavjud emas:         (topilmadi)
Ikki bosqichli tekshiruv nima uchun kerak

Ko'r indeks 8 baytga qisqartirilgan, demak to'qnashuvlar bo'lishi mumkin (3-bo'limdagi tug'ilgan kun paradoksi).

Agar faqat indeksga tayansangiz, boshqa mijozning yozuvi qaytishi mumkin - va bu jiddiy maxfiylik buzilishi.

Shuning uchun jarayon ikki bosqichli:

BosqichNima qiladiNima uchun
1. Indeks bo'yicha filtrNomzodlarni topadiTez - baza indeksidan foydalanadi
2. Ochib solishtirishAniq mosligini tekshiradiTo'g'ri - to'qnashuvni chetlab o'tadi

Ikkinchi bosqichda hash_equals ishlatilgani ham tasodifiy emas (2-bo'lim).

Konvert sxemasi va kalit almashtirish #

PHP
final class KonvertShifri
{
    /** @var array<int, string> versiya => asosiy kalit (KEK) */
    private array $kekLar;
    private int $joriyVersiya;

    /** @param array<int, string> $kekLar */
    public function __construct(array $kekLar, int $joriyVersiya)
    {
        $this->kekLar = $kekLar;
        $this->joriyVersiya = $joriyVersiya;
    }

    public function shifrla(string $matn): string
    {
        // 1. Har yozuvga YANGI ma'lumot kaliti (DEK)
        $dek = random_bytes(32);
        $nonce = random_bytes(12);
        $teg = '';

        $shifr = openssl_encrypt($matn, 'aes-256-gcm', $dek,
            OPENSSL_RAW_DATA, $nonce, $teg);

        // 2. DEK ni asosiy kalit (KEK) bilan shifrlaymiz
        $kekNonce = random_bytes(12);
        $kekTeg = '';
        $oralganDek = openssl_encrypt($dek, 'aes-256-gcm',
            $this->kekLar[$this->joriyVersiya], OPENSSL_RAW_DATA,
            $kekNonce, $kekTeg);

        return implode('.', [
            'k' . $this->joriyVersiya,
            base64_encode($kekNonce . $kekTeg . $oralganDek),
            base64_encode($nonce . $teg . $shifr),
        ]);
    }

    public function och(string $saqlangan): string
    {
        [$versiyaBelgisi, $oralganB64, $malumotB64] = explode('.', $saqlangan, 3);
        $versiya = (int) substr($versiyaBelgisi, 1);

        if (!isset($this->kekLar[$versiya])) {
            throw new RuntimeException("kalit versiyasi {$versiya} mavjud emas");
        }

        // 1. DEK ni ochamiz
        $oralgan = base64_decode($oralganB64);
        $dek = openssl_decrypt(substr($oralgan, 28), 'aes-256-gcm',
            $this->kekLar[$versiya], OPENSSL_RAW_DATA,
            substr($oralgan, 0, 12), substr($oralgan, 12, 16));

        if ($dek === false) {
            throw new RuntimeException('DEK ochilmadi');
        }

        // 2. Ma'lumotni ochamiz
        $malumot = base64_decode($malumotB64);
        $matn = openssl_decrypt(substr($malumot, 28), 'aes-256-gcm', $dek,
            OPENSSL_RAW_DATA, substr($malumot, 0, 12),
            substr($malumot, 12, 16));

        if ($matn === false) {
            throw new RuntimeException('malumot ochilmadi');
        }

        return $matn;
    }

    /** Kalit almashtirish: faqat DEK qayta o'raladi, ma'lumot tegilmaydi */
    public function kalitniYangila(string $saqlangan): string
    {
        [$versiyaBelgisi, $oralganB64, $malumotB64] = explode('.', $saqlangan, 3);
        $versiya = (int) substr($versiyaBelgisi, 1);

        $oralgan = base64_decode($oralganB64);
        $dek = openssl_decrypt(substr($oralgan, 28), 'aes-256-gcm',
            $this->kekLar[$versiya], OPENSSL_RAW_DATA,
            substr($oralgan, 0, 12), substr($oralgan, 12, 16));

        if ($dek === false) {
            throw new RuntimeException('DEK ochilmadi');
        }

        $yangiNonce = random_bytes(12);
        $yangiTeg = '';
        $yangiOralgan = openssl_encrypt($dek, 'aes-256-gcm',
            $this->kekLar[$this->joriyVersiya], OPENSSL_RAW_DATA,
            $yangiNonce, $yangiTeg);

        return implode('.', [
            'k' . $this->joriyVersiya,
            base64_encode($yangiNonce . $yangiTeg . $yangiOralgan),
            $malumotB64,          // MA'LUMOT O'ZGARMADI
        ]);
    }
}
PHP
$kekLar = [
    1 => hex2bin(str_repeat('11', 32)),
    2 => hex2bin(str_repeat('22', 32)),
];

// Eski kalit bilan shifrlangan yozuv
$eski = new KonvertShifri($kekLar, 1);
$yozuv = $eski->shifrla('Juda maxfiy hisobot matni');

korsat('kalit versiyasi', explode('.', $yozuv)[0]);
korsat('ochildi', $eski->och($yozuv));

// Kalit almashtirildi - endi 2-versiya joriy
$yangi = new KonvertShifri($kekLar, 2);
$yangilangan = $yangi->kalitniYangila($yozuv);

korsat('yangi versiya', explode('.', $yangilangan)[0]);
korsat('yangi kalit bilan', $yangi->och($yangilangan));

// Ma'lumot qismi o'zgarmagan
$eskiMalumot = explode('.', $yozuv)[2];
$yangiMalumot = explode('.', $yangilangan)[2];
korsat('malumot ozgardimi', $eskiMalumot === $yangiMalumot ? 'yoq' : 'HA');
Natija
kalit versiyasi:     k1
ochildi:             Juda maxfiy hisobot matni
yangi versiya:       k2
yangi kalit bilan:   Juda maxfiy hisobot matni
malumot ozgardimi:   yoq
Konvert sxemasining asosiy foydasi

Kalitni almashtirish uchun butun bazani qayta shifrlash shart emas.

To'g'ridan-to'g'ri shifrlashKonvert sxemasi
Kalit almashtirishHar yozuvni ochib qayta shifrlashFaqat o'ralgan DEK ni yangilash
10 million yozuv uchunSoatlab ishlovDaqiqalar
Katta fayllarFayl qayta yoziladiFayl tegilmaydi
Har yozuvda kalitBitta umumiy kalitHar yozuvga o'z kaliti

Oxirgi qator ham muhim: har yozuvning o'z DEK i bo'lgani uchun bitta DEK sizib chiqsa, faqat bitta yozuv ochiladi.

Bu sxema AWS KMS, Google Cloud KMS va HashiCorp Vault da aynan shunday ishlatiladi.

Nimalarni shifrlash kerak #

Ma'lumotShifrlashSabab
ParolYo'qXeshlanadi (4-bo'lim)
Karta raqamiHaTo'lov standartlari talabi
Pasport, JShShIRHaShaxsiy ma'lumot
Tibbiy yozuvlarHaAlohida himoya toifasi
EmailBog'liqQidiruv kerakmi?
Ism-familiyaOdatda yo'qDeyarli har so'rovda kerak
Jurnal yozuvlariTanlabIchida shaxsiy ma'lumot bo'lsa
Ommaviy ma'lumotYo'qFoydasiz murakkablik
Hamma narsani shifrlash - noto'g'ri strategiya

Shifrlash narx bilan keladi:

NarxIzoh
Qidiruv va tartiblash yo'qoladiKo'r indeks faqat aniq moslikni beradi
Hisobot va tahlil qiyinlashadiGROUP BY, SUM ishlamaydi
Nosozlik qidirish murakkabBazaga qarab hech narsa tushunmaysiz
Kalit yo'qolsa - ma'lumot yo'qoladiZaxira nusxa ham ochilmaydi
Ishlash tezligi pasayadiHar o'qishda ochish

Oxirgidan bittasi eng xavflisi. Kalitni yo'qotish - ma'lumotni butunlay yo'qotish bilan barobar. Kalitlarning zaxira nusxasi ma'lumot zaxirasidan alohida saqlanishi kerak.

Shuning uchun shifrlashni tanlab qo'llang: eng sezgir maydonlarga.

Amaliy topshiriq
  1. Tahdid modelini yozing: qaysi shifrlash turi nimadan himoyalaydi.
  2. Maydon shifrlovchi sinf yozing (versiya + nonce + teg + shifrmatn).
  3. AAD ga yozuv identifikatorini qo'shing va boshqa yozuvda ochishga urinib ko'ring.
  4. Bir xil qiymatni ikki marta shifrlab, natijalar farqini ko'ring.
  5. Nima uchun bu farq kerakli ekanini tushuntiring.
  6. Ko'r indeks sinfini yozing va normallashtirishni qo'shing.
  7. Ikki bosqichli qidiruvni amalga oshiring.
  8. Faqat indeksga tayanish nima uchun xavfli ekanini yozing.
  9. Konvert sxemasini yozing va kalitni almashtiring.
  10. Kalit almashtirilgach ma'lumot qismi o'zgarmaganini tekshiring.

Xulosa #

  • Shifrlashdan oldin tahdid modelini aniqlang: kimdan himoyalanmoqdasiz.
  • Disk shifri server o'chirilganda ishlaydi; ishlab turganda foyda bermaydi.
  • Ilova darajasidagi shifrlash SQL in'yeksiyasi va baza dumpidan himoyalaydi.
  • Ilova serveri buzilsa - hech qanday shifrlash yordam bermaydi.
  • Shifrlangan qiymat asl hajmdan ~1.4 barobar katta va ustunga indeks qo'yib bo'lmaydi.
  • Har shifrlashda boshqa natija chiqishi - to'g'ri xulq, lekin qidiruvni buzadi.
  • Ko'r indeks (HMAC) aniq moslik bo'yicha qidiruvni tiklaydi.
  • Indeks qisqartirilgani uchun qidiruv ikki bosqichli bo'lishi shart.
  • Konvert sxemasi kalitni almashtirishni bazani qayta shifrlashsiz imkonli qiladi.
  • Hamma narsani shifrlamang - kalit yo'qolsa ma'lumot ham yo'qoladi.

Keyingi bo'limda tokenlar va JWT ni ko'ramiz.

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.