18-bo‘lim

Kalitlarni boshqarish

Eng kuchli algoritm ham kalit noto'g'ri saqlansa foydasiz - saqlash, almashtirish va sizib chiqqanda nima qilish.

🕑 14 daqiqa o‘qish 📄 930 so‘z 👁 1 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Kalit qayerda bo'lmasligi kerak
  2. Kalitni to'g'ri yuklash
  3. Barmoq izi - kalitni oshkor qilmasdan aniqlash
  4. Kalit almashtirish
  5. Kalit yo'qolganda nima bo'ladi
  6. Kod ichidagi sirlarni topish
  7. Kalitlarni ajratish
  8. Tekshiruv ro'yxati
  9. Xulosa

Oldingi o'n yetti bo'limda algoritmlarni to'g'ri ishlatishni o'rgandik. Endi eng zaif halqaga keldik.

Amaliy statistika oddiy: kriptografiya buzilmaydi, kalitlar sizib chiqadi.

Kalit qayerda bo'lmasligi kerak #

PHP
function korsat(string $nom, string $qiymat): void
{
    printf("%-24s %s%s", $nom . ':', $qiymat, PHP_EOL);
}
PHP
$joylar = [
    ['Manba kodida', 'HALOKATLI', 'git tarixida abadiy qoladi'],
    ['Git ga qoshilgan .env', 'HALOKATLI', 'har bir nusxada bor'],
    ['Docker image ichida', 'JUDA YOMON', 'registrda hamma koradi'],
    ['Jurnal fayllarida', 'JUDA YOMON', 'markaziy jurnalga ketadi'],
    ['Xato hisobotlarida', 'JUDA YOMON', 'uchinchi tomon xizmatida'],
    ['Mijoz tomonida (JS)', 'HALOKATLI', 'har kim koradi'],
    ['Muhit ozgaruvchisida', 'qabul qilinadi', 'jarayon royxatida korinishi mumkin'],
    ['Huquqi 600 boigan faylda', 'yaxshi', 'faqat ilova foydalanuvchisi'],
    ['Sirlar boshqaruvchisida', 'ENG YAXSHI', 'audit, almashtirish, cheklov'],
    ['HSM yoki KMS da', 'ENG YAXSHI', 'kalit umuman chiqmaydi'],
];

printf("%-24s %-16s %s%s", 'QAYERDA', 'BAHO', 'IZOH', PHP_EOL);
echo str_repeat('-', 78), PHP_EOL;
foreach ($joylar as $j) {
    printf("%-24s %-16s %s%s", ...[...$j, PHP_EOL]);
}
Natija
QAYERDA                  BAHO             IZOH
------------------------------------------------------------------------------
Manba kodida             HALOKATLI        git tarixida abadiy qoladi
Git ga qoshilgan .env    HALOKATLI        har bir nusxada bor
Docker image ichida      JUDA YOMON       registrda hamma koradi
Jurnal fayllarida        JUDA YOMON       markaziy jurnalga ketadi
Xato hisobotlarida       JUDA YOMON       uchinchi tomon xizmatida
Mijoz tomonida (JS)      HALOKATLI        har kim koradi
Muhit ozgaruvchisida     qabul qilinadi   jarayon royxatida korinishi mumkin
Huquqi 600 boigan faylda yaxshi           faqat ilova foydalanuvchisi
Sirlar boshqaruvchisida  ENG YAXSHI       audit, almashtirish, cheklov
HSM yoki KMS da          ENG YAXSHI       kalit umuman chiqmaydi
Git tarixi - eng ko'p unutiladigan joy

Kalit bir marta kommit qilinsa, uni oddiy o'chirish yetarli emas:

KOD
git rm .env
git commit -m "kalitni olib tashladim"

Bu kalitni tarixdan olib tashlamaydi. U hali ham:

Qayerda qoladiKim ko'ra oladi
Eski kommitlardaRepozitoriyga kirgan har kim
Barcha klonlardaHar bir dasturchi
Fork lardaUlar ustidan nazorat yo'q
CI keshlaridaCI xizmati
GitHub kesh sahifalaridaBa'zan hamma

Tarixni tozalash (git filter-repo) mumkin, lekin u barcha kommit xeshlarini o'zgartiradi va hamma klonni buzadi.

To'g'ri javob bitta: kalitni darhol almashtiring. Tarixni tozalash - ikkinchi darajali ish.

Kalitni to'g'ri yuklash #

PHP
final class KalitOmbori
{
    /** @var array<string, string> */
    private array $kalitlar = [];

    /** @param array<string, string> $muhit */
    public function __construct(array $muhit)
    {
        foreach ($muhit as $nom => $qiymat) {
            if (!str_starts_with($nom, 'APP_KEY_')) {
                continue;
            }

            $baytlar = $this->tekshirVaOch($nom, $qiymat);
            $this->kalitlar[substr($nom, 8)] = $baytlar;
        }
    }

    private function tekshirVaOch(string $nom, string $qiymat): string
    {
        if ($qiymat === '') {
            throw new RuntimeException("{$nom} bosh");
        }

        // Muhit o'zgaruvchisi matn, kalit esa baytlar - base64 orqali
        $baytlar = base64_decode($qiymat, true);

        if ($baytlar === false) {
            throw new RuntimeException("{$nom} base64 emas");
        }
        if (strlen($baytlar) !== 32) {
            throw new RuntimeException(
                "{$nom} uzunligi " . strlen($baytlar) . " bayt, 32 kutilgan");
        }
        // Ochiq namunalar bilan chalkashmasin
        if ($baytlar === str_repeat("\x00", 32)) {
            throw new RuntimeException("{$nom} - nol kalit, sozlash unutilgan");
        }

        return $baytlar;
    }

    public function kalit(string $nom): string
    {
        return $this->kalitlar[$nom]
            ?? throw new RuntimeException("kalit topilmadi: {$nom}");
    }

    /** Jurnalga yozish uchun xavfsiz identifikator */
    public function barmoqIzi(string $nom): string
    {
        return substr(bin2hex(hash_hmac('sha256', 'barmoq-izi',
            $this->kalit($nom), true)), 0, 16);
    }

    /** @return list<string> */
    public function nomlar(): array
    {
        return array_keys($this->kalitlar);
    }
}
PHP
// Amalda: getenv() dan keladi. Bu yerda qat'iy namunalar.
$muhit = [
    'APP_KEY_V1' => base64_encode(hex2bin(str_repeat('11', 32))),
    'APP_KEY_V2' => base64_encode(hex2bin(str_repeat('22', 32))),
    'DB_HOST' => '127.0.0.1',        // e'tiborga olinmaydi
];

$ombor = new KalitOmbori($muhit);

korsat('yuklangan kalitlar', implode(', ', $ombor->nomlar()));
korsat('V1 barmoq izi', $ombor->barmoqIzi('V1'));
korsat('V2 barmoq izi', $ombor->barmoqIzi('V2'));

// Xato holatlari
foreach ([
    'qisqa kalit' => ['APP_KEY_X' => base64_encode('qisqa')],
    'base64 emas' => ['APP_KEY_X' => '!!! bu base64 emas !!!'],
    'bosh' => ['APP_KEY_X' => ''],
    'nol kalit' => ['APP_KEY_X' => base64_encode(str_repeat("\x00", 32))],
] as $nom => $yomon) {
    try {
        new KalitOmbori($yomon);
        printf('%-14s OTDI - KUTILMAGAN%s', $nom, PHP_EOL);
    } catch (RuntimeException $x) {
        printf('%-14s RAD: %s%s', $nom, $x->getMessage(), PHP_EOL);
    }
}
Natija
yuklangan kalitlar:      V1, V2
V1 barmoq izi:           b7c4de66026dc508
V2 barmoq izi:           93a2942d2a156842
qisqa kalit    RAD: APP_KEY_X uzunligi 5 bayt, 32 kutilgan
base64 emas    RAD: APP_KEY_X base64 emas
bosh           RAD: APP_KEY_X bosh
nol kalit      RAD: APP_KEY_X - nol kalit, sozlash unutilgan
Ishga tushishda tekshirish - eng arzon himoya

Yuqoridagi tekshiruvlar ilova ishga tushganda bajariladi, so'rov paytida emas.

Bu muhim farq:

Qachon tekshiriladiNatija
So'rov paytidaXato ishlab chiqarishda, foydalanuvchi oldida
Ishga tushishdaIlova umuman ko'tarilmaydi - deploy to'xtaydi

Ikkinchisi ancha yaxshi: noto'g'ri sozlangan versiya foydalanuvchiga yetib bormaydi.

Nol kalit tekshiruvi ham tasodifiy emas. Namuna .env.example faylida ko'pincha APP_KEY= bo'sh yoki nollar bilan to'ldirilgan bo'ladi va uni almashtirish unutiladi.

Barmoq izi - kalitni oshkor qilmasdan aniqlash #

PHP
$ombor = new KalitOmbori([
    'APP_KEY_V1' => base64_encode(hex2bin(str_repeat('11', 32))),
    'APP_KEY_V2' => base64_encode(hex2bin(str_repeat('22', 32))),
]);

// Jurnalga bunday yozish MUMKIN
echo "[info] shifrlash kaliti: ", $ombor->barmoqIzi('V1'), PHP_EOL;
echo "[info] imzo kaliti:      ", $ombor->barmoqIzi('V2'), PHP_EOL;

// Bunday yozish MUMKIN EMAS
echo PHP_EOL, 'XATO USUL (hech qachon qilmang):', PHP_EOL;
echo '  error_log("kalit: " . $kalit);', PHP_EOL;
echo '  var_dump($sozlama);   // ichida kalit bor', PHP_EOL;
echo '  throw new Exception("kalit notogri: {$kalit}");', PHP_EOL;
Natija
[info] shifrlash kaliti: b7c4de66026dc508
[info] imzo kaliti:      93a2942d2a156842

XATO USUL (hech qachon qilmang):
  error_log("kalit: " . $kalit);
  var_dump($sozlama);   // ichida kalit bor
  throw new Exception("kalit notogri: {$kalit}");
Kalitning hayot davri 1. Yaratish random_bytes(32) CSPRNG dan 2. Saqlash sirlar boshqaruvchisi yoki KMS 3. Ishlatish HKDF bilan ajratish har maqsadga alohida 4. Almashtirish yangi versiya qo'shiladi eskisi ochish uchun qoladi 5. Chiqarish hamma yozuv ko'chgach o'chiriladi Almashtirish davrida ikki kalit birga yashaydi v1 - faqat OCHISH uchun v2 - shifrlash VA ochish Yangi yozuvlar v2 bilan shifrlanadi. Eski yozuvlar o'qilganda asta-sekin v2 ga ko'chiriladi. Barcha yozuvlar ko'chgach - v1 o'chiriladi. Bu jarayon haftalar davom etishi mumkin. Kalit sizib chiqqanda - tartib bo'yicha 1. Yangi kalit yaratib, joriy qilish (eskisi ochish uchun qoladi) 2. Barcha ma'lumotni yangi kalitga ko'chirish 3. Eski kalit bilan berilgan barcha token va sessiyalarni bekor qilish 4. Jurnallarni tekshirish: kalit qachondan beri ochiq edi, kim foydalangan
Almashtirish - bir martalik hodisa emas, jarayon

Kalit almashtirish #

PHP
final class VersiyaliShifr
{
    /** @param array<int, string> $kalitlar */
    public function __construct(
        private readonly array $kalitlar,
        private readonly int $joriy,
    ) {
        if (!isset($kalitlar[$joriy])) {
            throw new InvalidArgumentException('joriy versiya kalitlari yoq');
        }
    }

    public function shifrla(string $matn): string
    {
        $nonce = random_bytes(12);
        $teg = '';
        $shifr = openssl_encrypt($matn, 'aes-256-gcm',
            $this->kalitlar[$this->joriy], OPENSSL_RAW_DATA, $nonce, $teg,
            (string) $this->joriy);   // versiya AAD ga ham kiradi

        return $this->joriy . ':' . base64_encode($nonce . $teg . $shifr);
    }

    public function och(string $saqlangan): string
    {
        [$versiya, $b64] = explode(':', $saqlangan, 2);
        $v = (int) $versiya;

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

        $x = base64_decode($b64);
        $matn = openssl_decrypt(substr($x, 28), 'aes-256-gcm',
            $this->kalitlar[$v], OPENSSL_RAW_DATA,
            substr($x, 0, 12), substr($x, 12, 16), $versiya);

        if ($matn === false) {
            throw new RuntimeException('ochib bolmadi');
        }

        return $matn;
    }

    public function eskirganmi(string $saqlangan): bool
    {
        return (int) explode(':', $saqlangan, 2)[0] !== $this->joriy;
    }
}
PHP
$kalitlar = [
    1 => hex2bin(str_repeat('11', 32)),
    2 => hex2bin(str_repeat('22', 32)),
];

// Eski kalit joriy bo'lgan davr
$eski = new VersiyaliShifr($kalitlar, 1);
$yozuv = $eski->shifrla('Maxfiy hisobot');
korsat('versiya', explode(':', $yozuv)[0]);

// Kalit almashtirildi
$yangi = new VersiyaliShifr($kalitlar, 2);

korsat('eski yozuv ochildi', $yangi->och($yozuv));
korsat('eskirganmi', $yangi->eskirganmi($yozuv) ? 'ha' : 'yoq');

// O'qish paytida ko'chirish
$kochirilgan = $yangi->eskirganmi($yozuv)
    ? $yangi->shifrla($yangi->och($yozuv))
    : $yozuv;

korsat('yangi versiya', explode(':', $kochirilgan)[0]);
korsat('mazmuni saqlandi', $yangi->och($kochirilgan));
Natija
versiya:                 1
eski yozuv ochildi:      Maxfiy hisobot
eskirganmi:              ha
yangi versiya:           2
mazmuni saqlandi:        Maxfiy hisobot
"O'qish paytida ko'chirish" naqshi

Bu 4-bo'limdagi password_needs_rehash bilan bir xil g'oya:

KOD
$malumot = $shifr->och($yozuv);

if ($shifr->eskirganmi($yozuv)) {
    $ombor->yangila($id, $shifr->shifrla($malumot));
}

Foydasi: butun bazani bir vaqtda qayta shifrlash kerak emas. Ma'lumot o'qilgan sari ko'chib boradi.

Kamchiligi: kamdan-kam o'qiladigan yozuvlar uzoq vaqt eski kalitda qoladi. Shuning uchun ma'lum muddatdan keyin qolganlarini fon jarayonida majburiy ko'chirish kerak.

Ikkalasini birga ishlatish eng amaliy yechim.

Kalit yo'qolganda nima bo'ladi #

PHP
$kalitlar = [2 => hex2bin(str_repeat('22', 32))];   // 1-versiya yo'q!

$shifr = new VersiyaliShifr($kalitlar, 2);

// 1-versiya bilan shifrlangan eski yozuv
$eskiYozuv = '1:' . base64_encode(random_bytes(64));

try {
    $shifr->och($eskiYozuv);
} catch (RuntimeException $x) {
    korsat('natija', 'RAD: ' . $x->getMessage());
}

echo PHP_EOL, 'Kalit yoqolsa - malumot ham YOQOLADI.', PHP_EOL;
echo 'Zaxira nusxa ham yordam bermaydi: u ham shifrlangan.', PHP_EOL;
Natija
natija:                  RAD: kalit versiyasi 1 mavjud emas

Kalit yoqolsa - malumot ham YOQOLADI.
Zaxira nusxa ham yordam bermaydi: u ham shifrlangan.
Kalitlarning zaxirasi - alohida masala

Ma'lumot zaxirasi va kalit zaxirasi bir joyda saqlanmasligi kerak:

Bir joydaAlohida
O'g'ri ikkalasini oladiFaqat bittasi foyda bermaydi
Shifrlashning ma'nosi yo'qoladiHimoya saqlanadi

Lekin qarama-qarshi xavf ham bor: kalit zaxirasi yo'qolsa, ma'lumot butunlay yo'qoladi.

Amaliy yechim:

ChoraIzoh
Kalitni kamida ikki mustaqil joyda saqlashBittasi yo'qolsa ikkinchisi qoladi
Tiklashni muntazam sinashZaxira ishlashini bilish
Kalitni bo'lish (secret sharing)N dan K qismi yetarli
Qog'ozda, seyfdaEng muhim kalitlar uchun

Uchinchi variant Shamir sxemasi deb ataladi: kalit beshta qismga bo'linadi, tiklash uchun uchtasi yetarli. Hech kim yolg'iz kalitga ega bo'lmaydi.

Kod ichidagi sirlarni topish #

PHP
// Soddalashtirilgan qidiruvchi - kodda qolib ketgan sirlarni aniqlaydi
function sirlarniQidir(string $kod): array
{
    $naqshlar = [
        'AWS kaliti' => '/AKIA[0-9A-Z]{16}/',
        'Ochiq maxfiy kalit' => '/-----BEGIN [A-Z ]*PRIVATE KEY-----/',
        'Uzun hex kalit' => '/[\'"][0-9a-f]{64}[\'"]/',
        'Qattiq yozilgan parol' => '/(parol|password|passwd)\s*=\s*[\'"][^\'"]{8,}[\'"]/i',
        'Slack tokeni' => '/xox[baprs]-[0-9a-zA-Z-]{10,}/',
        'Umumiy API kaliti' => '/(api[_-]?key|secret)\s*=\s*[\'"][^\'"]{16,}[\'"]/i',
    ];

    $topilgan = [];
    foreach ($naqshlar as $nom => $naqsh) {
        if (preg_match($naqsh, $kod)) {
            $topilgan[] = $nom;
        }
    }

    return $topilgan;
}

$namunalar = [
    'toza kod' => '$kalit = getenv("APP_KEY");',
    'qattiq parol' => '$parol = "SuperMaxfiy123";',
    'hex kalit' => '$k = "' . str_repeat('ab', 32) . '";',
    'api kaliti' => '$api_key = "sk_live_abcdefghijklmnop";',
    'pem kalit' => "-----BEGIN PRIVATE KEY-----\nMIIE...",
];

foreach ($namunalar as $nom => $kod) {
    $t = sirlarniQidir($kod);
    printf('%-14s %s%s', $nom,
        $t === [] ? 'toza' : 'TOPILDI: ' . implode(', ', $t), PHP_EOL);
}
Natija
toza kod       toza
qattiq parol   TOPILDI: Qattiq yozilgan parol
hex kalit      TOPILDI: Uzun hex kalit
api kaliti     TOPILDI: Umumiy API kaliti
pem kalit      TOPILDI: Ochiq maxfiy kalit
Buni qo'lda emas, avtomatik qiling

Yuqoridagi kod tamoyilni ko'rsatadi. Amalda tayyor vositalar ishlatiladi va ular kommitgacha ishga tushadi:

QayerdaVosita turi
Dasturchi mashinasidapre-commit ilgagi
CI quvuridaSirlarni skanerlash bosqichi
Repozitoriy darajasidaPlatformaning o'z skaneri
Tarixni tekshirishButun tarix bo'yicha skanerlash

Eng samarali joy - birinchi qator: kommit umuman yaratilmasa, tarixni tozalash kerak bo'lmaydi.

Yolg'on ijobiy natijalar bo'ladi (test ma'lumotlari, namunalar). Ularni ro'yxatga qo'shish mumkin, lekin har istisnoni ko'rib chiqish kerak - aynan shu yerda haqiqiy sirlar yashirinib qoladi.

Kalitlarni ajratish #

PHP
// Bitta asosiy kalitdan hamma maqsad uchun kalitlar (6-bo'lim)
$asosiy = hex2bin(str_repeat('42', 32));

$maqsadlar = [
    'sessiya-imzosi', 'fayl-shifri', 'vebxuk-imzosi',
    'kuki-shifri', 'parol-qalampiri',
];

printf("%-18s %s%s", 'MAQSAD', 'BARMOQ IZI', PHP_EOL);
echo str_repeat('-', 38), PHP_EOL;

foreach ($maqsadlar as $m) {
    $k = hash_hkdf('sha256', $asosiy, 32, 'v1:' . $m);
    printf("%-18s %s%s", $m,
        substr(bin2hex(hash('sha256', $k, true)), 0, 16), PHP_EOL);
}

echo PHP_EOL, 'Bitta kalit sizsa - faqat bitta maqsad zarar koradi', PHP_EOL;
Natija
MAQSAD             BARMOQ IZI
--------------------------------------
sessiya-imzosi     99a000e9ba95ac2a
fayl-shifri        d388387f84356e95
vebxuk-imzosi      d875500a33e07732
kuki-shifri        02579e8585bf8278
parol-qalampiri    a39a81bed34201e0

Bitta kalit sizsa - faqat bitta maqsad zarar koradi

Tekshiruv ro'yxati #

SavolJavob "yo'q" bo'lsa
Kalitlar kodda yo'qmiDarhol almashtiring
.env gitignore damiTekshiring va tarixni skanerlang
Har muhitda alohida kalitmiAjrating (dev / test / prod)
Kalit uzunligi tekshiriladimiIshga tushish tekshiruvini qo'shing
Jurnalga kalit tushmaydimiBarmoq iziga o'ting
Almashtirish sinab ko'rilganmiSinab ko'ring - u ishlamasligi mumkin
Kalit zaxirasi bormiYarating va tiklashni sinang
Sizib chiqish rejasi bormiYozing - hodisa paytida vaqt bo'lmaydi
Amaliy topshiriq
  1. Kalit saqlanmasligi kerak bo'lgan beshta joyni sanang.
  2. Nima uchun git rm kalitni tarixdan olib tashlamasligini tushuntiring.
  3. Muhit o'zgaruvchisidan kalit yuklaydigan sinf yozing.
  4. To'rtta xato holatini (qisqa, base64 emas, bo'sh, nol) tekshiring.
  5. Kalit barmoq izini hisoblang va uni jurnalga yozing.
  6. Versiyali shifrlash sinfini yozing.
  7. Kalitni almashtiring va eski yozuvni yangi versiyaga ko'chiring.
  8. Kalit yo'qolgan holatni modellashtiring.
  9. Kodda qolib ketgan sirlarni topuvchi oddiy skaner yozing.
  10. Tekshiruv ro'yxatini o'z loyihangizga qo'llang.

Xulosa #

  • Kriptografiya buzilmaydi, kalitlar sizib chiqadi.
  • Kalit hech qachon kodda, git da, jurnalda yoki mijoz tomonida bo'lmasligi kerak.
  • git rm kalitni tarixdan olib tashlamaydi - yagona to'g'ri javob almashtirish.
  • Kalitni ishga tushishda tekshiring: uzunlik, format, namuna qiymat emasligi.
  • Jurnalga kalitning o'zini emas, barmoq izini yozing.
  • Almashtirish - jarayon: yangi versiya joriy bo'ladi, eskisi ochish uchun qoladi.
  • O'qish paytida ko'chirish butun bazani bir vaqtda qayta shifrlashdan qutqaradi.
  • Kalit yo'qolsa - ma'lumot ham yo'qoladi; zaxira nusxa yordam bermaydi.
  • Kalit zaxirasi ma'lumot zaxirasidan alohida saqlanadi, lekin u ham bo'lishi shart.
  • Sirlarni skanerlash kommitgacha ishlashi kerak - keyin kech bo'ladi.

Keyingi bo'limda barcha bo'limlardagi xatolarni bir joyga to'playmiz.

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.