20-bo‘lim

Amaliy loyiha - sirlar sandig'i

O'n to'qqiz bo'limni bitta ishlaydigan tizimda birlashtirish - shifrlangan omborcha, kalit almashtirish va buzilishni aniqlash.

🕑 13 daqiqa o‘qish 📄 777 so‘z 👁 1 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. 1-qism: asosiy kalit
  2. 2-qism: konvert va sir yozuvi
  3. 3-qism: buzilishni sezadigan audit jurnali
  4. Buzilishni aniqlash
  5. Audit jurnalining buzilishini aniqlash
  6. Asosiy parolni almashtirish
  7. Xavfsizlik xossalari
  8. Yakuniy sinov
  9. Xulosa

Yakuniy bo'limda sirlar sandig'i quramiz: parollar, API kalitlari va boshqa maxfiy qiymatlarni shifrlangan holda saqlaydigan omborcha.

Har qism oldingi bo'limlardan keladi:

QismBo'lim
Asosiy paroldan kalit (PBKDF2)6
Har sirga alohida kalit (konvert sxemasi)16
AEAD shifrlash va AAD8
Tasodifiy nonce9, 15
Nom bo'yicha qidiruv (ko'r indeks)16
Doimiy vaqtda solishtirish2
HMAC zanjirli audit jurnali5
Kalit versiyalari va almashtirish18

1-qism: asosiy kalit #

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

final class AsosiyKalit
{
    // DIQQAT: darslikda tez ishlashi uchun past qiymat.
    // Ishlab chiqarishda kamida 600 000 (6-bo'lim).
    private const TAKRORLAR = 100000;
    private const TUZ_UZUNLIGI = 16;

    private function __construct(
        public readonly string $kek,
        public readonly string $tuz,
    ) {}

    public static function yangi(string $parol): self
    {
        return self::paroldan($parol, random_bytes(self::TUZ_UZUNLIGI));
    }

    public static function paroldan(string $parol, string $tuz): self
    {
        if (strlen($parol) < 12) {
            throw new InvalidArgumentException(
                'asosiy parol kamida 12 belgi bolishi kerak');
        }

        $xom = hash_pbkdf2('sha256', $parol, $tuz,
            self::TAKRORLAR, 32, true);

        return new self($xom, $tuz);
    }

    /** Har maqsadga alohida kalit (6-bo'lim) */
    public function maqsadKaliti(string $maqsad): string
    {
        return hash_hkdf('sha256', $this->kek, 32, 'sandiq-v1:' . $maqsad);
    }

    public function barmoqIzi(): string
    {
        return substr(bin2hex(hash_hmac('sha256', 'barmoq',
            $this->kek, true)), 0, 16);
    }
}
PHP
// Qat'iy tuz - natija takrorlanishi uchun
$tuz = hex2bin(str_repeat('5a', 16));
$asosiy = AsosiyKalit::paroldan('juda-uzun-asosiy-parol', $tuz);

korsat('KEK barmoq izi', $asosiy->barmoqIzi());
korsat('shifr kaliti', substr(bin2hex($asosiy->maqsadKaliti('shifr')), 0, 24) . '...');
korsat('indeks kaliti', substr(bin2hex($asosiy->maqsadKaliti('indeks')), 0, 24) . '...');
korsat('jurnal kaliti', substr(bin2hex($asosiy->maqsadKaliti('jurnal')), 0, 24) . '...');

// Bir xil parol + bir xil tuz = bir xil kalit
$yana = AsosiyKalit::paroldan('juda-uzun-asosiy-parol', $tuz);
korsat('takrorlanadimi', $yana->barmoqIzi() === $asosiy->barmoqIzi() ? 'ha' : 'yoq');

// Qisqa parol rad etiladi
try {
    AsosiyKalit::paroldan('qisqa', $tuz);
} catch (InvalidArgumentException $x) {
    korsat('qisqa parol', 'RAD: ' . $x->getMessage());
}
Natija
KEK barmoq izi:          197620d2623dabcd
shifr kaliti:            44e316c5944393147306259d...
indeks kaliti:           9bc03971652c8e8777eaee70...
jurnal kaliti:           a11feb7dd884197b17aaea7d...
takrorlanadimi:          ha
qisqa parol:             RAD: asosiy parol kamida 12 belgi bolishi kerak

2-qism: konvert va sir yozuvi #

PHP
final readonly class SirYozuvi
{
    public function __construct(
        public string $nom,
        public string $indeks,
        public string $oralganKalit,
        public string $shifrlangan,
        public int $versiya,
        public int $yaratilgan,
    ) {}
}

final class Sandiq
{
    /** @var array<string, SirYozuvi> */
    private array $yozuvlar = [];

    public function __construct(
        private AsosiyKalit $asosiy,
        private readonly AuditJurnali $jurnal,
        private int $versiya = 1,
    ) {}

    public function qosh(string $nom, string $qiymat, int $vaqt): void
    {
        // 1. Har sirga ALOHIDA ma'lumot kaliti (16-bo'lim)
        $dek = random_bytes(32);

        // 2. Sirni DEK bilan shifrlaymiz, AAD ga nom va versiya
        $nonce = random_bytes(12);
        $teg = '';
        $shifr = openssl_encrypt($qiymat, 'aes-256-gcm', $dek,
            OPENSSL_RAW_DATA, $nonce, $teg, "{$nom}|v{$this->versiya}");

        if ($shifr === false) {
            throw new RuntimeException('shifrlash muvaffaqiyatsiz');
        }

        // 3. DEK ni asosiy kalit bilan o'raymiz
        $kekNonce = random_bytes(12);
        $kekTeg = '';
        $oralgan = openssl_encrypt($dek, 'aes-256-gcm',
            $this->asosiy->maqsadKaliti('shifr'), OPENSSL_RAW_DATA,
            $kekNonce, $kekTeg, "dek|{$nom}");

        if ($oralgan === false) {
            throw new RuntimeException('DEK oralmadi');
        }

        $indeks = $this->indeks($nom);

        $this->yozuvlar[$indeks] = new SirYozuvi(
            nom: $nom,
            indeks: $indeks,
            oralganKalit: $kekNonce . $kekTeg . $oralgan,
            shifrlangan: $nonce . $teg . $shifr,
            versiya: $this->versiya,
            yaratilgan: $vaqt,
        );

        $this->jurnal->yoz('qoshildi', $nom, $vaqt);
    }

    public function ol(string $nom, int $vaqt): string
    {
        $yozuv = $this->yozuvlar[$this->indeks($nom)] ?? null;

        if ($yozuv === null) {
            $this->jurnal->yoz('topilmadi', $nom, $vaqt);
            throw new RuntimeException('sir topilmadi');
        }

        // 1. DEK ni ochamiz
        $o = $yozuv->oralganKalit;
        $dek = openssl_decrypt(substr($o, 28), 'aes-256-gcm',
            $this->asosiy->maqsadKaliti('shifr'), OPENSSL_RAW_DATA,
            substr($o, 0, 12), substr($o, 12, 16), "dek|{$yozuv->nom}");

        if ($dek === false) {
            $this->jurnal->yoz('DEK ochilmadi', $nom, $vaqt);
            throw new RuntimeException('sandiq buzilgan yoki parol notogri');
        }

        // 2. Sirni ochamiz
        $s = $yozuv->shifrlangan;
        $qiymat = openssl_decrypt(substr($s, 28), 'aes-256-gcm', $dek,
            OPENSSL_RAW_DATA, substr($s, 0, 12), substr($s, 12, 16),
            "{$yozuv->nom}|v{$yozuv->versiya}");

        if ($qiymat === false) {
            $this->jurnal->yoz('BUZILGAN', $nom, $vaqt);
            throw new RuntimeException('butunlik tekshiruvi otmadi');
        }

        $this->jurnal->yoz('oqildi', $nom, $vaqt);

        return $qiymat;
    }

    /** Ko'r indeks - nom bo'yicha qidiruv uchun (16-bo'lim) */
    private function indeks(string $nom): string
    {
        return bin2hex(substr(hash_hmac('sha256',
            mb_strtolower(trim($nom), 'UTF-8'),
            $this->asosiy->maqsadKaliti('indeks'), true), 0, 8));
    }

    /** @return list<array{nom: string, versiya: int}> */
    public function royxat(): array
    {
        $natija = [];
        foreach ($this->yozuvlar as $y) {
            $natija[] = ['nom' => $y->nom, 'versiya' => $y->versiya];
        }
        usort($natija, static fn(array $a, array $b): int
            => strcmp($a['nom'], $b['nom']));

        return $natija;
    }

    public function soni(): int
    {
        return count($this->yozuvlar);
    }

    /** Buzilishni sinash uchun - odatda bunday metod bo'lmaydi */
    public function baytniBuz(string $nom, int $orin): void
    {
        $i = $this->indeks($nom);
        $y = $this->yozuvlar[$i];
        $s = $y->shifrlangan;
        $s[$orin] = chr(ord($s[$orin]) ^ 0xff);

        $this->yozuvlar[$i] = new SirYozuvi($y->nom, $y->indeks,
            $y->oralganKalit, $s, $y->versiya, $y->yaratilgan);
    }
}

3-qism: buzilishni sezadigan audit jurnali #

PHP
final class AuditJurnali
{
    /** @var list<array{vaqt: int, amal: string, nom: string, mac: string}> */
    private array $qatorlar = [];
    private string $oxirgiMac;

    public function __construct(private readonly string $kalit)
    {
        // Zanjir boshlanishi
        $this->oxirgiMac = str_repeat("\x00", 32);
    }

    public function yoz(string $amal, string $nom, int $vaqt): void
    {
        $qator = "{$vaqt}|{$amal}|{$nom}";

        // Har yozuv OLDINGI MAC ni ham qamrab oladi - zanjir hosil bo'ladi
        $mac = hash_hmac('sha256', $this->oxirgiMac . $qator,
            $this->kalit, true);

        $this->qatorlar[] = [
            'vaqt' => $vaqt, 'amal' => $amal,
            'nom' => $nom, 'mac' => $mac,
        ];

        $this->oxirgiMac = $mac;
    }

    /** Zanjirni boshidan tekshiradi */
    public function butunmi(): bool
    {
        $oldingi = str_repeat("\x00", 32);

        foreach ($this->qatorlar as $q) {
            $kutilgan = hash_hmac('sha256',
                $oldingi . "{$q['vaqt']}|{$q['amal']}|{$q['nom']}",
                $this->kalit, true);

            if (!hash_equals($kutilgan, $q['mac'])) {
                return false;
            }

            $oldingi = $q['mac'];
        }

        return true;
    }

    public function soni(): int
    {
        return count($this->qatorlar);
    }

    /** @return list<string> */
    public function korinish(): array
    {
        $natija = [];
        foreach ($this->qatorlar as $i => $q) {
            $natija[] = sprintf('%d. %s %-12s %s', $i + 1,
                gmdate('H:i:s', $q['vaqt']), $q['amal'], $q['nom']);
        }

        return $natija;
    }

    /** Sinash uchun: o'rtadagi yozuvni o'chirib yuborish */
    public function yozuvniOchir(int $orin): void
    {
        array_splice($this->qatorlar, $orin, 1);
    }
}

function sandiqYarat(int $vaqt): array
{
    $asosiy = AsosiyKalit::paroldan('juda-uzun-asosiy-parol',
        hex2bin(str_repeat('5a', 16)));
    $jurnal = new AuditJurnali($asosiy->maqsadKaliti('jurnal'));
    $sandiq = new Sandiq($asosiy, $jurnal);

    $sandiq->qosh('baza-paroli', 'S3rv3r!Parol#2025', $vaqt);
    $sandiq->qosh('api-kaliti', 'sk_live_abcdefghijklmnop', $vaqt + 1);
    $sandiq->qosh('smtp-paroli', 'pochta-maxfiy-2025', $vaqt + 2);

    return [$sandiq, $jurnal, $asosiy];
}
PHP
$vaqt = 1735689600;
[$sandiq, $jurnal] = sandiqYarat($vaqt);

korsat('sandiqdagi sirlar', $sandiq->soni() . ' ta');

foreach ($sandiq->royxat() as $s) {
    printf('  %-14s (v%d)%s', $s['nom'], $s['versiya'], PHP_EOL);
}

echo PHP_EOL;
korsat('api-kaliti', $sandiq->ol('api-kaliti', $vaqt + 10));
korsat('katta harf bilan', $sandiq->ol('API-KALITI', $vaqt + 11));
korsat('jurnal yozuvlari', $jurnal->soni() . ' ta');
korsat('jurnal butunmi', $jurnal->butunmi() ? 'ha' : 'YOQ');
Natija
sandiqdagi sirlar:       3 ta
  api-kaliti     (v1)
  baza-paroli    (v1)
  smtp-paroli    (v1)

api-kaliti:              sk_live_abcdefghijklmnop
katta harf bilan:        sk_live_abcdefghijklmnop
jurnal yozuvlari:        5 ta
jurnal butunmi:          ha
Sirlar sandig'i - kalitlar ierarxiyasi Asosiy parol faqat odam boshida PBKDF2 600k KEK (32 bayt) HKDF shifr kaliti indeks kaliti jurnal kaliti DEK larni o'raydi har sirga alohida DEK nom bo'yicha qidiruv HMAC ko'r indeks audit zanjiri har yozuv oldingisiga bog'lanadi Sir shifrlangan: AES-GCM(DEK, sir, AAD = nom|versiya) · DEK o'ralgan: AES-GCM(shifr_kaliti, DEK) Parol almashtirilganda faqat o'ralgan DEK lar yangilanadi - sirlarning o'zi tegilmaydi.
Ikki qatlamli kalit: parol o'zgarsa ham ma'lumot qayta shifrlanmaydi

Buzilishni aniqlash #

PHP
$vaqt = 1735689600;
[$sandiq, $jurnal] = sandiqYarat($vaqt);

// Sirlardan biri diskda buzildi
$sandiq->baytniBuz('smtp-paroli', 30);

foreach (['baza-paroli', 'smtp-paroli', 'api-kaliti'] as $nom) {
    try {
        $q = $sandiq->ol($nom, $vaqt + 20);
        printf('%-14s OK (%d belgi)%s', $nom, strlen($q), PHP_EOL);
    } catch (RuntimeException $x) {
        printf('%-14s RAD: %s%s', $nom, $x->getMessage(), PHP_EOL);
    }
}

echo PHP_EOL;
korsat('jurnalda yozuv', $jurnal->soni() . ' ta');
echo 'Buzilish jurnalga tushdi:', PHP_EOL;
foreach ($jurnal->korinish() as $q) {
    if (str_contains($q, 'BUZILGAN')) {
        echo '  ', $q, PHP_EOL;
    }
}
Natija
baza-paroli    OK (17 belgi)
smtp-paroli    RAD: butunlik tekshiruvi otmadi
api-kaliti     OK (24 belgi)

jurnalda yozuv:          6 ta
Buzilish jurnalga tushdi:
  5. 00:00:20 BUZILGAN     smtp-paroli
Buzilgan yozuv qolganlariga ta'sir qilmadi

Faqat smtp-paroli rad etildi. Qolgan ikkitasi normal ochildi.

Sabab - har sirning o'z DEK i bor. Bitta yozuvning buzilishi boshqalariga tegmaydi.

Agar hammasi bitta kalit bilan bitta blokda shifrlangan bo'lsa, bitta bayt buzilishi butun sandiqni yaroqsiz qilardi.

Bu konvert sxemasining yana bir foydasi (16-bo'lim).

Audit jurnalining buzilishini aniqlash #

PHP
$vaqt = 1735689600;
[$sandiq, $jurnal] = sandiqYarat($vaqt);

$sandiq->ol('api-kaliti', $vaqt + 10);
$sandiq->ol('baza-paroli', $vaqt + 11);

korsat('yozuvlar soni', $jurnal->soni() . ' ta');
korsat('zanjir butunmi', $jurnal->butunmi() ? 'ha' : 'YOQ');

echo PHP_EOL, 'Jurnal:', PHP_EOL;
foreach ($jurnal->korinish() as $q) {
    echo '  ', $q, PHP_EOL;
}

// Hujumchi o'z izini o'chirmoqchi bo'ladi
$jurnal->yozuvniOchir(3);

echo PHP_EOL, 'Ortadagi yozuv ochirilgandan keyin:', PHP_EOL;
korsat('yozuvlar soni', $jurnal->soni() . ' ta');
korsat('zanjir butunmi', $jurnal->butunmi() ? 'ha' : 'YOQ - BUZILGAN');
Natija
yozuvlar soni:           5 ta
zanjir butunmi:          ha

Jurnal:
  1. 00:00:00 qoshildi     baza-paroli
  2. 00:00:01 qoshildi     api-kaliti
  3. 00:00:02 qoshildi     smtp-paroli
  4. 00:00:10 oqildi       api-kaliti
  5. 00:00:11 oqildi       baza-paroli

Ortadagi yozuv ochirilgandan keyin:
yozuvlar soni:           4 ta
zanjir butunmi:          YOQ - BUZILGAN
Zanjir nima uchun ishlaydi

Har yozuvning MAC i oldingi yozuvning MAC ini ham qamrab oladi:

KOD
mac[i] = HMAC(kalit, mac[i-1] + qator[i])

Demak o'rtadagi yozuvni o'chirish yoki o'zgartirish undan keyingi barcha MAC larni yaroqsiz qiladi.

Hujumchi izini yashirish uchun butun zanjirni qayta hisoblashi kerak - buning uchun esa jurnal kaliti kerak.

Bu buzilishni sezadigan (tamper-evident) jurnal. Diqqat: u buzilishning oldini olmaydi, faqat uni aniqlaydi.

To'liq himoya uchun jurnal:

ChoraNima beradi
Faqat qo'shiladigan (append-only) saqlashO'chirishni qiyinlashtiradi
Boshqa serverga nusxalashMahalliy o'zgarishni aniqlaydi
MAC ni tashqi tizimga yuborishButun faylni almashtirishni aniqlaydi
Har kun oxirgi MAC ni e'lon qilishOrqaga qaytarib yozishni imkonsiz qiladi

Asosiy parolni almashtirish #

PHP
final class ParolAlmashtiruvchi
{
    /**
     * Faqat O'RALGAN DEK lar qayta o'raladi.
     * Sirlarning o'zi umuman tegilmaydi.
     */
    public static function almashtir(
        string $oralganKalit,
        string $nom,
        AsosiyKalit $eski,
        AsosiyKalit $yangi,
    ): string {
        // 1. Eski kalit bilan DEK ni ochamiz
        $dek = openssl_decrypt(substr($oralganKalit, 28), 'aes-256-gcm',
            $eski->maqsadKaliti('shifr'), OPENSSL_RAW_DATA,
            substr($oralganKalit, 0, 12), substr($oralganKalit, 12, 16),
            "dek|{$nom}");

        if ($dek === false) {
            throw new RuntimeException('eski parol notogri');
        }

        // 2. Yangi kalit bilan qayta o'raymiz
        $nonce = random_bytes(12);
        $teg = '';
        $qayta = openssl_encrypt($dek, 'aes-256-gcm',
            $yangi->maqsadKaliti('shifr'), OPENSSL_RAW_DATA,
            $nonce, $teg, "dek|{$nom}");

        return $nonce . $teg . $qayta;
    }
}
PHP
$eski = AsosiyKalit::paroldan('juda-uzun-asosiy-parol',
    hex2bin(str_repeat('5a', 16)));
$yangi = AsosiyKalit::paroldan('yangi-juda-uzun-parol-2025',
    hex2bin(str_repeat('6b', 16)));

korsat('eski KEK', $eski->barmoqIzi());
korsat('yangi KEK', $yangi->barmoqIzi());

// Bitta sir uchun DEK yasaymiz va o'raymiz
$dek = hex2bin(str_repeat('99', 32));
$nonce = hex2bin(str_repeat('01', 12));
$teg = '';
$oralgan = $nonce . $teg;
$shifr = openssl_encrypt($dek, 'aes-256-gcm',
    $eski->maqsadKaliti('shifr'), OPENSSL_RAW_DATA, $nonce, $teg, 'dek|test');
$oralgan = $nonce . $teg . $shifr;

// Qayta o'rash
$yangiOralgan = ParolAlmashtiruvchi::almashtir($oralgan, 'test', $eski, $yangi);

korsat('oralgan DEK uzunligi', strlen($yangiOralgan) . ' bayt');
korsat('eski bilan ochiladimi',
    openssl_decrypt(substr($yangiOralgan, 28), 'aes-256-gcm',
        $eski->maqsadKaliti('shifr'), OPENSSL_RAW_DATA,
        substr($yangiOralgan, 0, 12), substr($yangiOralgan, 12, 16),
        'dek|test') === false ? 'yoq - togri' : 'HA - XATO');

$ochilgan = openssl_decrypt(substr($yangiOralgan, 28), 'aes-256-gcm',
    $yangi->maqsadKaliti('shifr'), OPENSSL_RAW_DATA,
    substr($yangiOralgan, 0, 12), substr($yangiOralgan, 12, 16), 'dek|test');

korsat('yangi bilan ochiladimi', $ochilgan !== false ? 'ha' : 'yoq');
korsat('DEK saqlandimi', hash_equals($dek, (string) $ochilgan) ? 'ha' : 'yoq');
Natija
eski KEK:                197620d2623dabcd
yangi KEK:               45fdebbf83db5051
oralgan DEK uzunligi:    60 bayt
eski bilan ochiladimi:   yoq - togri
yangi bilan ochiladimi:  ha
DEK saqlandimi:          ha
Bu nima uchun tez

Parol almashtirishda faqat 60 bayt qayta shifrlanadi (o'ralgan DEK), sirning o'zi qanchalik katta bo'lishidan qat'i nazar.

Sirlar soniTo'g'ridan-to'g'riKonvert sxemasi
100Hammasini qayta shifrlash100 × 60 bayt
1 000 000Soatlab ishlovDaqiqalar
10 GB fayl10 GB qayta yozish60 bayt

Oxirgi qator sxemaning butun ma'nosini ko'rsatadi.

Xavfsizlik xossalari #

PHP
$xossalar = [
    ['Sirlar diskda ochiq emas', 'AES-256-GCM', 'ha'],
    ['Ozgartirish aniqlanadi', 'GCM tegi', 'ha'],
    ['Bir sir buzilsa qolgani ishlaydi', 'har sirga DEK', 'ha'],
    ['Nom boyicha qidiruv', 'HMAC kor indeks', 'ha'],
    ['Sir nomlari diskda ochiqmi', 'indeks - HMAC', 'yoq'],
    ['Jurnal buzilishi aniqlanadi', 'HMAC zanjiri', 'ha'],
    ['Parol almashtirish tez', 'konvert sxemasi', 'ha'],
    ['Parol bilmasdan ochish', 'PBKDF2 600k', 'yoq'],
    ['Server buzilsa himoya', 'kalit xotirada', 'YOQ'],
    ['Zaxira nusxa xavfsizmi', 'shifrlangan', 'ha'],
];

printf("%-34s %-20s %s%s", 'XOSSA', 'QANDAY', 'BORMI', PHP_EOL);
echo str_repeat('-', 64), PHP_EOL;
foreach ($xossalar as $x) {
    printf("%-34s %-20s %s%s", ...[...$x, PHP_EOL]);
}
Natija
XOSSA                              QANDAY               BORMI
----------------------------------------------------------------
Sirlar diskda ochiq emas           AES-256-GCM          ha
Ozgartirish aniqlanadi             GCM tegi             ha
Bir sir buzilsa qolgani ishlaydi   har sirga DEK        ha
Nom boyicha qidiruv                HMAC kor indeks      ha
Sir nomlari diskda ochiqmi         indeks - HMAC        yoq
Jurnal buzilishi aniqlanadi        HMAC zanjiri         ha
Parol almashtirish tez             konvert sxemasi      ha
Parol bilmasdan ochish             PBKDF2 600k          yoq
Server buzilsa himoya              kalit xotirada       YOQ
Zaxira nusxa xavfsizmi             shifrlangan          ha
Oxirgidan ikkinchi qatorga diqqat

"Server buzilsa himoya" - YO'Q.

Sandiq ishlab turganda asosiy kalit xotirada bo'ladi. Hujumchi ilova jarayoniga kirsa, u kalitni o'qiy oladi.

Bu 16-bo'limdagi tahdid modelining takrori. Sandiq quyidagilardan himoyalaydi:

HimoyalaydiHimoyalamaydi
O'g'irlangan disk yoki zaxiraIshlab turgan serverga kirish
Fayl tizimiga o'qish huquqiIlova zaifligi orqali kirish
Nusxa ko'chirilgan ma'lumotKlaviatura kuzatuvchisi
Ma'lumotni jimgina o'zgartirishXotira dumpi

Real sirlar boshqaruvchilari (Vault, KMS) bu bo'shliqni qisman yopadi: kalit umuman ilova jarayoniga tushmaydi, shifrlash alohida xizmatda bajariladi.

Lekin u yerda ham hujumchi ilova nomidan so'rov yubora oladi. Farqi shundaki, bu jurnalga tushadi va cheklovlar qo'llanadi.

Yakuniy sinov #

PHP
$vaqt = 1735689600;
[$sandiq, $jurnal, $asosiy] = sandiqYarat($vaqt);

$sinovlar = [];

// 1. Barcha sirlar to'g'ri ochiladi
$hammasi = true;
foreach (['baza-paroli', 'api-kaliti', 'smtp-paroli'] as $n) {
    try {
        $sandiq->ol($n, $vaqt + 5);
    } catch (RuntimeException) {
        $hammasi = false;
    }
}
$sinovlar['barcha sirlar ochiladi'] = $hammasi;

// 2. Mavjud bo'lmagan sir rad etiladi
try {
    $sandiq->ol('yoq-bunday', $vaqt + 6);
    $sinovlar['nomavjud sir rad etiladi'] = false;
} catch (RuntimeException) {
    $sinovlar['nomavjud sir rad etiladi'] = true;
}

// 3. Buzilgan sir rad etiladi
$sandiq->baytniBuz('api-kaliti', 30);
try {
    $sandiq->ol('api-kaliti', $vaqt + 7);
    $sinovlar['buzilgan sir rad etiladi'] = false;
} catch (RuntimeException) {
    $sinovlar['buzilgan sir rad etiladi'] = true;
}

// 4. Buzilgandan keyin ham boshqalari ishlaydi
try {
    $sandiq->ol('baza-paroli', $vaqt + 8);
    $sinovlar['qolganlari ishlaydi'] = true;
} catch (RuntimeException) {
    $sinovlar['qolganlari ishlaydi'] = false;
}

// 5. Jurnal zanjiri butun
$sinovlar['jurnal zanjiri butun'] = $jurnal->butunmi();

// 6. Jurnal buzilsa aniqlanadi
$jurnal->yozuvniOchir(2);
$sinovlar['jurnal buzilishi aniqlanadi'] = !$jurnal->butunmi();

foreach ($sinovlar as $nom => $natija) {
    printf('%-30s %s%s', $nom, $natija ? 'OTDI' : 'YIQILDI', PHP_EOL);
}

printf('%sjami: %d/%d%s', PHP_EOL,
    count(array_filter($sinovlar)), count($sinovlar), PHP_EOL);
Natija
barcha sirlar ochiladi         OTDI
nomavjud sir rad etiladi       OTDI
buzilgan sir rad etiladi       OTDI
qolganlari ishlaydi            OTDI
jurnal zanjiri butun           OTDI
jurnal buzilishi aniqlanadi    OTDI

jami: 6/6
Yakuniy loyiha
  1. Ushbu sandiqni noldan yozing (kod ko'chirmang, qayta yozing).
  2. PBKDF2 takrorlarini 600 000 ga oshiring va vaqtni o'lchang.
  3. Yangi sir turi qo'shing (masalan fayl mazmuni).
  4. Sirni o'chirish amalini qo'shing va uni jurnalga yozing.
  5. Bir sirni buzib, faqat o'sha rad etilishini tekshiring.
  6. Jurnal zanjirining o'rtasidagi yozuvni o'zgartiring.
  7. Zanjir buzilganini aniqlaydigan testni yozing.
  8. Asosiy parolni almashtiring va barcha sirlar ochilishini tekshiring.
  9. Sandiq faylga saqlanadigan va o'qiladigan qilib kengaytiring.
  10. Tahdid modelini yozing: bu sandiq nimadan himoyalaydi va nimadan yo'q.

Xulosa #

  • Sandiq ikki qatlamli kalit ishlatadi: paroldan KEK, har sirga DEK.
  • Asosiy paroldan kalit PBKDF2 bilan olinadi (ishlab chiqarishda 600 000+ takror).
  • KEK dan HKDF orqali uchta ajratilgan kalit chiqariladi: shifr, indeks, jurnal.
  • Har sir o'z DEK i bilan shifrlanadi - bir yozuvning buzilishi qolganlariga tegmaydi.
  • AAD ga nom va versiya kiritilgani sirlarni almashtirishga yo'l qo'ymaydi.
  • Ko'r indeks nom bo'yicha qidiruvni beradi, nomlarni esa ochiq saqlamaydi.
  • Audit jurnali HMAC zanjiri bilan bog'langan - o'rtadagi yozuvni o'chirish aniqlanadi.
  • Zanjir buzilishning oldini olmaydi, faqat uni aniqlaydi.
  • Parol almashtirilganda faqat o'ralgan DEK lar yangilanadi - sirlar tegilmaydi.
  • Sandiq ishlab turgan serverga kirishdan himoya bermaydi - bu chegara har doim yodda tutilsin.

Darslik shu bilan yakunlandi. Eng muhim xulosa 1-bo'limdan o'zgarmadi: algoritmni o'zingiz yozmang, lekin uni to'g'ri birlashtirishni biling.

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.