16-bo‘lim
Ma'lumotlarni saqlashda shifrlash
Bazadagi maydonni shifrlash - konvert sxemasi, kalit almashtirish va shifrlangan maydon bo'yicha qidirish.
Ushbu bo‘lim mundarijasi
Bazani shifrlash haqida gapirishdan oldin bitta savolga javob berish kerak: kimdan himoyalanmoqdamiz?
Bu savolga javob bermasdan qilingan shifrlash ko'pincha foyda bermaydi.
Tahdid modeli #
| Tahdid | Disk shifri | Ustun shifri | Ilova shifri |
|---|---|---|---|
| O'g'irlangan disk | Himoyalaydi | Himoyalaydi | Himoyalaydi |
| Zaxira nusxa sizib chiqdi | Yo'q | Himoyalaydi | Himoyalaydi |
| SQL in'yeksiyasi orqali dump | Yo'q | Yo'q | Himoyalaydi |
| Baza administratori | Yo'q | Qisman | Himoyalaydi |
| Ilova serveri buzildi | Yo'q | Yo'q | Yo'q |
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 #
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;
}
}
$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());
}
shakli: v1.<nonce>.<teg>.<shifrmatn>
qismlar soni: 4 ta
versiya: v1
uzunligi: 69 belgi
ochilgan: 8600123456781234
boshqa yozuvda: RAD: butunlik tekshiruvi otmadi
Shifrlangan qiymat asl matndan uzunroq:
| Qism | Hajmi |
|---|---|
| Versiya belgisi | 3 belgi (v1.) |
| Nonce (base64) | 16 belgi |
| Teg (base64) | 24 belgi |
| Shifrmatn (base64) | asl hajm × 4/3 |
| Nuqtalar | 3 belgi |
16 baytlik karta raqami uchun jami ~70 belgi.
Amaliy tavsiyalar:
| Masala | Yechim |
|---|---|
| Ustun turi | VARBINARY yoki TEXT |
| Hajm | Asl hajm × 1.4 + 50 |
| Indeks | To'g'ridan-to'g'ri qo'yib bo'lmaydi |
ORDER BY | Ishlamaydi |
LIKE qidiruv | Ishlamaydi |
Oxirgi uchtasi keyingi bo'limning mavzusi.
Shifrlangan maydon bo'yicha qidirish #
$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;
1-yozuv uzunligi: 73 belgi
2-yozuv uzunligi: 73 belgi
bir xilmi: YOQ
SELECT ... WHERE email = ? ishlamaydi:
har shifrlashda nonce boshqa - qiymat ham boshqa
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'rinadi | Misol |
|---|---|
| Qaysi yozuvlarda bir xil qiymat bor | 500 ta mijozda bir xil manzil |
| Har qiymat necha marta uchraydi | Chastota tahlili |
| Qiymatlar taqsimoti | Kam 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 #
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));
}
}
$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;
'[email protected]' 9c4fc3266a446023
'[email protected]' 9c4fc3266a446023
'[email protected]' 9c4fc3266a446023
'[email protected]' b6e5c6e08717ddfe
birinchi uchtasi bir xil - normallashtirish ishladi
To'liq misol: shifrlangan jadval #
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);
}
}
$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');
jadvalda: 4 ta yozuv
malika qidiruvi: 2
katta harf bilan: 2
mavjud emas: (topilmadi)
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:
| Bosqich | Nima qiladi | Nima uchun |
|---|---|---|
| 1. Indeks bo'yicha filtr | Nomzodlarni topadi | Tez - baza indeksidan foydalanadi |
| 2. Ochib solishtirish | Aniq mosligini tekshiradi | To'g'ri - to'qnashuvni chetlab o'tadi |
Ikkinchi bosqichda hash_equals ishlatilgani ham tasodifiy
emas (2-bo'lim).
Konvert sxemasi va kalit almashtirish #
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
]);
}
}
$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');
kalit versiyasi: k1
ochildi: Juda maxfiy hisobot matni
yangi versiya: k2
yangi kalit bilan: Juda maxfiy hisobot matni
malumot ozgardimi: yoq
Kalitni almashtirish uchun butun bazani qayta shifrlash shart emas.
| To'g'ridan-to'g'ri shifrlash | Konvert sxemasi | |
|---|---|---|
| Kalit almashtirish | Har yozuvni ochib qayta shifrlash | Faqat o'ralgan DEK ni yangilash |
| 10 million yozuv uchun | Soatlab ishlov | Daqiqalar |
| Katta fayllar | Fayl qayta yoziladi | Fayl tegilmaydi |
| Har yozuvda kalit | Bitta umumiy kalit | Har 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'lumot | Shifrlash | Sabab |
|---|---|---|
| Parol | Yo'q | Xeshlanadi (4-bo'lim) |
| Karta raqami | Ha | To'lov standartlari talabi |
| Pasport, JShShIR | Ha | Shaxsiy ma'lumot |
| Tibbiy yozuvlar | Ha | Alohida himoya toifasi |
| Bog'liq | Qidiruv kerakmi? | |
| Ism-familiya | Odatda yo'q | Deyarli har so'rovda kerak |
| Jurnal yozuvlari | Tanlab | Ichida shaxsiy ma'lumot bo'lsa |
| Ommaviy ma'lumot | Yo'q | Foydasiz murakkablik |
Shifrlash narx bilan keladi:
| Narx | Izoh |
|---|---|
| Qidiruv va tartiblash yo'qoladi | Ko'r indeks faqat aniq moslikni beradi |
| Hisobot va tahlil qiyinlashadi | GROUP BY, SUM ishlamaydi |
| Nosozlik qidirish murakkab | Bazaga qarab hech narsa tushunmaysiz |
| Kalit yo'qolsa - ma'lumot yo'qoladi | Zaxira nusxa ham ochilmaydi |
| Ishlash tezligi pasayadi | Har 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.
- Tahdid modelini yozing: qaysi shifrlash turi nimadan himoyalaydi.
- Maydon shifrlovchi sinf yozing (versiya + nonce + teg + shifrmatn).
- AAD ga yozuv identifikatorini qo'shing va boshqa yozuvda ochishga urinib ko'ring.
- Bir xil qiymatni ikki marta shifrlab, natijalar farqini ko'ring.
- Nima uchun bu farq kerakli ekanini tushuntiring.
- Ko'r indeks sinfini yozing va normallashtirishni qo'shing.
- Ikki bosqichli qidiruvni amalga oshiring.
- Faqat indeksga tayanish nima uchun xavfli ekanini yozing.
- Konvert sxemasini yozing va kalitni almashtiring.
- 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.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
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.