Как устроен AES

О чём эта статья



Долгое время я считал, что криптографические алгоритмы шифрования и хеширования, вроде AES и MD5, устроены очень сложно и написать их совсем не просто, даже имея под рукой полную документацию. Запутанные реализации этих алгоритмов на разных языках программирования только укрепляли это мнение. Но недавно у меня появилось много свободного времени и я решил разобраться в этих алгоритмах и написать их. Оказалось, что они очень просто устроены и для их реализации нужно совсем немного времени.

В этой статье я напишу как устроен алгоритм шифрования AES (которого иногда называют Rijndael) и напишу его на JavaScript. Почему на JavaScript? Чтобы запустить программу на этом языке, нужен только браузер в котором вы читаете эту статью. Чтобы запустить программу, скажем, на C, нужен компилятор и найдётся совсем мало желающих, готовых потратить время на компиляцию кода из какой то статьи. В конце есть ссылка по которой можно скачать архив с html страницей и несколькими js файлами — это пример реализации AES на JavaScript.



Как применить AES



Этот алгоритм преобразует один 128-битный блок в другой, используя секретный ключ который нужен для такого преобразования. Для расшифровки полученного 128-битного блока используют второе преобразование с тем же секретным ключом. Выглядит это так:

cipher = encrypt(block, key) // шифруем block с помощью key
block = decrypt(cipher, key) // расшифровываем cipher с помощью key


Размер блока всегда равен 128 бит. Размер ключа также имеет фиксированный размер. Чтобы зашифровать произвольный текст любым паролем можно поступить так:

  • получить хеш от пароля
  • преобразовать хеш в ключ по правилам описанным в стандарте AES
  • разбить текст на блоки по 128 бит
  • зашифровать каждый блок функцией cipher


Это можно записать так:

hash = md5(password) // MD5 хеш имеет длину 128 бит
key = keyexpansion(hash) // преобразуем хеш в ключ
blocks = split(text, 16) // разбить текст на блоки по 16 байт

for (i = 0; i < blocks.length; i++)
cipher[i] = encrypt(blocks[i], key)


Чтобы расшифровать массив блоков cipher нужно применить к каждому блоку decrypt:

hash = md5(password)
key = keyexpansion(hash)

for (i = 0; i < cipher.length; i++)
blocks[i] = decrypt(cipher[i], key)

text = merge(blocks) // соединить все блоки в одну строку


Конечно, длина текста может быть не кратна 128 битам. В таких случаях можно дополнить текст нулями до нужной длины, а в зашифрованные данные добавить несколько байт с зашифрованным размером оригинального текста. Функции aes.encrypt и aes.decrypt в файле aes.js в примере используют этот подход.

Поле GF(28)



AES активно использует так называемое конечное поле GF(28). Чтобы написать AES на JavaScript не обязательно знать, что это за поле, но если вы хотите лучше понять AES, прочтите этот раздел.

Поле GF(28) это числа 0..255 для которых определили особое умножение и особое сложение. Возмём какое нибудь число из этого поля и представим его в виде восьми битов: a = a7a6a5a4a3a2a1a0. Точно также представим число b. Сложение a и b это известная побитовая операция xor:

a + b = a xor b

У сложения есть простые свойства:

a + a = 0
-a = 0 - a = a
a - b = a + (-b) = a + b


Умножение определяется сложнее. Запишем многочлены с коэффициентами из битов этих чисел:

p = a7x7 + a6x6 + a5x5 + a4x4 + a3x3 + a2x2 + a1x + a0
q = b7x7 + b6x6 + b5x5 + b4x4 + b3x3 + b2x2 + b1x + b0


Теперь перемножим эти два многочлена и найдём остаток от деления на m:

m = x8 + x4 + x3 + x + 1
r = pq mod (m)


Почему выбран именно такой m? У этого многочлена есть только два делителя-многочлена на которых он делится без остатка: единица и он сам. По аналогии с простыми числами, многочлен m «простой». Находить остаток от деления можно также как для обычных чисел: для этого достаточно уметь умножать, складывать и вычитать многочлены, причём сложение и вычитание производят по правилам GF(28), т.е. сложение и вычитание многочленов это xor между каждой парой коэффициентов. Вот два примера:

x3 + x2 + 1 mod (x3 + 1) = x2 // нужно один раз отнять x3+1
x3 + x2 + 1 mod (x2 + 1) = (x3 + x2 + 1) - (x + 1)(x2 + 1) = -x


Многочлен r представим в виде

r = r7x7 + r6x6 + r5x5 + r4x4 + r3x3 + r2x2 + r1x + r0


Его 8 коэффициентов представляют собой 8-битовое число из поля GF(28) и это число называется произведением a•b. В отличие от сложения, умножение нельзя найти парой простых побитовых операций. Однако умножение на произвольный многочлен в поле GF(28) можно свести к умножению на многочлен x, а умножить на x можно несколькими побитовыми операциями, о чём пойдёт речь ниже.

Для обозначения многочленов в GF(28) используют 16-ричные цифры. Например

m = x8 + x4 + x3 + x + 1 = 100011011 = 0x011b = {01}{1b}


Умножить на многочлен x = {02} в поле GF(28) очень просто. Рассмотрим произведение:

xp = x(a7x7 + a6x6 + a5x5 + a4x4 + a3x3 + a2x2 + a1x + a0) =
a7x8 + a6x7 + a5x6 + a4x5 + a3x4 + a2x3 + a1x<2 + a0x
p = a7a6a5a4a3a2a1a0
xp = a7a6a5a4a3a2a1a00 // это сдвиг влево на один бит


Теперь нужно найти остаток от деления на m. Если бит a7 = 1, то нужно один раз вычесть m. Если a7 = 0 то вычитать ничего не нужно. Итак:

r = xp mod (m) = xp - m если a7 = 1
r = xp mod (m) = xp если a7 = 0


Умножение на x можно записать такой функцией:

gf.xtime = function(b)
{
	var highbit = b & 0x80
	var shl = (b << 1) & 0xff
	return highbit == 0 ? shl : shl ^ 0x1b
}


Зная как умножать на x можно умножить на любой другой многочлен. Для примера найдём a•b где a = {3c}, b = {a1}:

b = {a1} = 10100001 = {80} + {20} + {01}
a•b = a•{80} + a•{20} + a•{01} = a•x7 + a•x5 + a =
a•{02}•{02}•{02}•{02}•{02}•{02}•{02} + a•{02}•{02}•{02}•{02}•{02} + a =
{29} + {c1} + {3c} = {d4}


Осталась одна простая операция в поле GF(28). У любого байта b, кроме нуля, есть обратный байт a = b-1 который обладает свойством a•b = {01}. Все три функции для работы с полем — умножение на x, умножение двух произвольных байтов и нахождение обратного — я собрал в маленькую библиотеку gf на JavaScript.

Таблица SBox



Эта таблица представляет собой 256-байтый массив и используется для замены одного байта другим. Не обязательно понимать как она получается, потому что в код можно просто скопировать этот массив. Чтобы узнать чему равен элемент SBox[b] нужно три действия:

  1. найти обратный байт к b в поле GF(28) (ноль оставить без изменений)
  2. умножить результат состоящий из восьми битов на матрицу 8×8 из 64 битов
  3. добавить {63}


В сумме эти три действия дают афинное преобразование:




Несложно понять как построена эта матрица из битов. Для умножения битов нужно применять «and», для сложения — «xor». Например:

r0 = b0 + b4 + b5 + b6 + b7 + 1


Функцию sbox я написал так:

aes.sbox = function(b)
{
	var m = 0xf8
	var r = 0
	var q = gf.inv(b) || 0
	
	for (var i = 0; i < 8; i++)
	{
		r = (r << 1) | bits.xorbits(q & m)
		m = (m >> 1) | ((m & 1) << 7)
	}
	
	return r ^ 0x63
}


Построенная таблица выглядит так:

63 7c 77 7b f2 6b 6f c5 30 01 67 2b fe d7 ab 76
ca 82 c9 7d fa 59 47 f0 ad d4 a2 af 9c a4 72 c0
b7 fd 93 26 36 3f f7 cc 34 a5 e5 f1 71 d8 31 15
04 c7 23 c3 18 96 05 9a 07 12 80 e2 eb 27 b2 75
09 83 2c 1a 1b 6e 5a a0 52 3b d6 b3 29 e3 2f 84
53 d1 00 ed 20 fc b1 5b 6a cb be 39 4a 4c 58 cf
d0 ef aa fb 43 4d 33 85 45 f9 02 7f 50 3c 9f a8
51 a3 40 8f 92 9d 38 f5 bc b6 da 21 10 ff f3 d2
cd 0c 13 ec 5f 97 44 17 c4 a7 7e 3d 64 5d 19 73
60 81 4f dc 22 2a 90 88 46 ee b8 14 de 5e 0b db
e0 32 3a 0a 49 06 24 5c c2 d3 ac 62 91 95 e4 79
e7 c8 37 6d 8d d5 4e a9 6c 56 f4 ea 65 7a ae 08
ba 78 25 2e 1c a6 b4 c6 e8 dd 74 1f 4b bd 8b 8a
70 3e b5 66 48 03 f6 0e 61 35 57 b9 86 c1 1d 9e
e1 f8 98 11 69 d9 8e 94 9b 1e 87 e9 ce 55 28 df
8c a1 89 0d bf e6 42 68 41 99 2d 0f b0 54 bb 16


Её можно просто скопировать в код, как часто делают, а можно вычислять функцией sbox по мере надобности.

Таблица InvSBox



Для дешифрования текста AES использует таблицу обратную к SBox. Таблица InvSBox обладает одним свойством: InvSBox[SBox[i]] = i. InvSBox выглядит так:

52 09 6a d5 30 36 a5 38 bf 40 a3 9e 81 f3 d7 fb
7c e3 39 82 9b 2f ff 87 34 8e 43 44 c4 de e9 cb
54 7b 94 32 a6 c2 23 3d ee 4c 95 0b 42 fa c3 4e
08 2e a1 66 28 d9 24 b2 76 5b a2 49 6d 8b d1 25
72 f8 f6 64 86 68 98 16 d4 a4 5c cc 5d 65 b6 92
6c 70 48 50 fd ed b9 da 5e 15 46 57 a7 8d 9d 84
90 d8 ab 00 8c bc d3 0a f7 e4 58 05 b8 b3 45 06
d0 2c 1e 8f ca 3f 0f 02 c1 af bd 03 01 13 8a 6b
3a 91 11 41 4f 67 dc ea 97 f2 cf ce f0 b4 e6 73
96 ac 74 22 e7 ad 35 85 e2 f9 37 e8 1c 75 df 6e
47 f1 1a 71 1d 29 c5 89 6f b7 62 0e aa 18 be 1b
fc 56 3e 4b c6 d2 79 20 9a db c0 fe 78 cd 5a f4
1f dd a8 33 88 07 c7 31 b1 12 10 59 27 80 ec 5f
60 51 7f a9 19 b5 4a 0d 2d e5 7a 9f 93 c9 9c ef
a0 e0 3b 4d ae 2a f5 b0 c8 eb bb 3c 83 53 99 61
17 2b 04 7e ba 77 d6 26 e1 69 14 63 55 21 0c 7d


Виды AES



Алгоритм AES преобразует блок длиной 128 битов в другой блок той же длины. Для преобразования применяется расписание ключей w получаемое из ключа. 128-битный блок в AES представляется в виде матрицы 4×Nb. Стандарт допускает только одно значение Nb = 4, поэтому длина блока всегда 128 бит, хотя алгоритм может работать с любым Nb. Длина ключа равна 4Nk байт. Алгоритм шифрования блока состоит из Nr раундов — применений одной и той же группы преобразований к 128-битному блоку данных. Стандарт допускает следующие комбинации этих трёх параметров:

Nk Nb Nr
AES-128 4 4 10
AES-192 6 4 12
AES-256 8 4 14


Преобразование KeyExpansion



Для шифрования текста AES применяет не пароль или хеш от пароля, а так называемое «расписание ключей» получаемое из ключа. Это расписание можно представить как Nr + 1 матриц размера 4×Nb. Алгоритм шифрования делает Nr + 1 шагов и на каждом шаге он, помимо других действий, берёт одну матрицу 4×Nb из «расписания» и поэлементно добавляет её к блоку данных.

Шифрование блока данных



Алгоритм шифрования получает на вход 128-битный блок данных input и расписание ключей w, которое получается после KeyExpansion. 16-байтый input он записывает в виде матрицы s размера 4×Nb, которая называется состоянием AES, и затем Nr раз применяет к этой матрице 4 преобразования. В конце он записывает матрицу в виде массива и подаёт его на выход — это зашифрованный блок. Каждое из четырёх преобразований очень простое.

  1. AddRoundKey берёт из расписания ключей одну матрицу размера 4×Nb и поэлементно добавляет её к матрице состояния. Если два раза применить AddRoundKey, то ничего не изменится, поэтому преобразование обратное к AddRoundKey это оно само.
  2. SubBytes заменяет каждый элемент матрицы состояния соответвующим элементом таблицы SBox: sij = SBox[sij]. Преобразование SubBytes обратимо. Обратное к нему находится с помощью таблицы InvSBox.
  3. ShiftRows сдвигает i-ую строку матрицы s на i позиций влево, считая i с нуля. Обратное преобразование InvShiftRows сдвигает строки вправо.
  4. MixColumns умножает каждый столбец матрицы s слева на особую матрицу размера 4×4:




    Для шифрования используют [a b c d] = [{02} {03} {01} {01}]. Можно проверить, что преобразование обратное к MixColumns[{02} {03} {01} {01}] это MixColumns[{0e} {0b} {0d} {09}].


Схематично шифрование можно изобразить так:

AddRoundKey(0)	

for (var i = 1; i <= Nr - 1; i++)
{			
	SubBytes()
	ShiftRows()
	MixColumns([0x02, 003, 0x01, 0x01])
	AddRoundKey(i)
}

SubBytes()
ShiftRows()
AddRoundKey(Nr)


Расшифровка



Как видно, для шифрования блока данных AES последовательно применяет к нему много обратимых преобразований. Для расшифровки нужно применить обратные преобразования в обратном порядке.

Немного оптимизации



Функция sbox имеет всего 256 возможных входных значений и 256 возможных выходных значений. Чтобы не вычислять много раз sbox для одного аргумента, нужно кешировать результаты. На JavaScript это несложно сделать даже не меняя код написанный ранее. Для этого нужно всего лишь дописать ниже вот это:

Function.prototype.cached = function()
{
	var old = this
	var cache = {}
	
	return function(x)
	{
		if (cache[x] !== undefined)
			return cache[x]
			
		cache[x] = old(x)
		return cache[x]
	}
}

aes.sbox = aes.sbox.cached()


Этот код заменяет sbox функцией которая кеширует результаты sbox. Тоже самое можно сделать для любой функции, например для invsbox и rcon. Этот же приём можно применить для функции gf.mul которая умножает два байта в поле GF(28), но в этом случае размер кеша будет равен 256×256 элементов, что довольно много.

Ссылки



Документация к AES на английском называется FIPS 197.

Поделиться публикацией
Комментарии 40
    0
    Интересный и несложный алгоритм. Спасибо.

    P.S. Переместите в блог «Алгоритмы».
      0
      По поводу «несложный» отписал ниже. Попробуйте придумать свой уникальный и криптостойкий алгоритм шифрования, пуст даже простой. Поймете, что не все так просто как кажется на первый взгляд.
      +1
      Мы в свое время в институте тоже разрабатывали в качестве курсовой работы несложные алгоритмы наподобие DES — предмет назывался«Основы криптографии» или как-то так.

      Кстати не так давно разбирался с RSA и Rijndael — но исключительно как пользователсь уже готовых классов .NET Framework. Просто понадобилось в процессе подготовки к сдаче проекта защитить все конфигурационные файлы от просмотра и модификации. Как выяснилось это все достаточно несложно — за один день разобрался и написал. Если нужно — могу написать об этом статью.
        +2
        Мне было бы интересно про RSA почитать. С этим алгоритмом я ещё не разбирался.
          0
          В RSA матчасть повеселее, вам понравится.
            +2
            RSA намного проще, главное вспомнить малую теорему Ферма.

            Вообще не припомню не одного сверх-сложного криптографического алгоритма. Когда все подробно распишут — на первый взгляд просто. Но попробуйте придумать что-то аналогичное... Сразу поймете что не так все просто, как кажется на первый взгляд. Даже оценить криптостойкость алгоритма — уже мало кто сможет.
              –1
              Да, RSA — он хитрый местами. Бывает, что и не сразу поймешь/вспомнишь как оно работает.
              Тоже вот недавно столкнулся с его применением при помощи wincrypt. Там, правда, не успел все реализовать за один день, так разбирался в том, как же все таки работает это виндовая библиотека, скрывая от пользователя все, что можно и нельзя.
                +4
                RSA вообще примитивен :)
                там всего несколько простейших формул, и арифметика достаточно простая
                для справки опишу:
                основной ключ, принято называть N, modulus, модуль — является произведением 2х больших (длиной в половину от желаемого размера ключа) простых чисел p и q:
                N = p * q
                этот N является общей частью и публичного и приватного ключей

                далее вычисляется вторая часть приватного ключа (D, экспонента приватного ключа) — вычисляетя значение (p-1)*(q-1) и выбирается значение E, по принципу 1<E<(p-1)*(q-1)
                обычно для E берут число 65537, оно будет публичной экспонентой, второй частью открытого ключа.
                ну и наконец вычисляется D по формуле (E*D) % ((p-1)*(q-1)) = 1, в качестве D подходит любое целочисленное решение данного уравнения.

                таким образом, получаются 2 ключа: E,N — публичный и D,N — приватный.

                сам алгоритм шифрования еще проще.

                сообщение M (длиной не более N) шифруется по формуле
                C = M^E % N (зашифрованное сообщение C = остатку от деления на N сообщения M в степени E)
                расшифровывается по формуле M = C^D % N
                т.е. зашифровать сообщение может любой, обладающий открытым ключем, а прочитать — только владелец приватного ключа.

                так-же пара ключей может быть использована для «цифрововой подписи»:
                пдопись S = M^D % N, проверка M = S ^ E % N
                т.е. создать «подписть» сообщения может только автор, а расшифровать и проверить — любой обладающий открытым ключем.
                вот и весь RSA…

                  +1
                  >>вычисляется D по формуле (E*D) % ((p-1)*(q-1)) = 1

                  Кстати, d может так-же вычисляться по такой формуле

                  (e*d*g) % ((p-1)*(q-1)) = g

                  Т.е. добавляется параметр g (обычно менее 10). При таком d RSA продолжает корректно работать.
                    +1
                    Дак он для того и добавляется, чтобы его перебором проще целое D найти. :)
                      +4
                      Нет. Вы c k путаете.

                      Эту тонкость мало кто знает. Обычно g равен единице, но иногда (в некоторых реализациях) принимают другое значение. Я когда атаки Винера реализовывал — сначала облажался, т.к. не знал про это g и что оно может быть отличным от единицы.

                      Обычно формула такая:

                      d*e = 1 + k*f (f — функция эйлера от n, k — любое число, чтобы получилось целое d)

                      А в случае с добавлением g, она принимает вид:

                      d*e*g = g + k*f

                      Вот с таким добавлением g RSA все равно работает и на практике это g иногда добавляется (в частности, ключ WebMoney Keeper Classic его использует, а вот Windows CSP не использует).

                      Вы говорите просто — а оказывается не все так просто. Тонкостей, которые не описаны в wiki народ то и не знает — минусы ставит.
                        0
                        Угу, посмотрел внимательнее, да. Даже использовал уже это когда генерацию ключей свою делал…
                    +1
                    Из асимметричной криптографии ElGamal в несколько раз проще и доступнее для понимания чем RSA.

                    Во-первых, ElGamal основан на математической неразрешимости только одной задачи, а не двух как RSA.
                    Во-вторых, математика генерации ключа гораздо проще, а операция из «длинной арифметики» вообще всего одна — возведение в степень по длинному модулю.
                    0
                    Посмотрите, ниже отписал про g. Он примитивен до тех пор, пока не вдаешься в детали… Дьявол в деталях.
                –1
                Большое спасибо за пост. Неплохо разбираться в алгоритмах шифрования, особенно когда временами нужно шифровать «на лету» исполняемые файлы.
                  –1
                  Хорошая статья. Спасибо. Интересно было читать.
                    +11
                    Спасибо за статью!

                    Однако, теперь вам нужно писать следующую. Сказали А — говорите и Б ;)
                    Всё-таки вы чересчур упростили проблему. На алгоритм в том виде, как вы описали (а именно, режим ECB), возможны разнообразные атаки. Например: тот факт, что блоки у вас не зависят друг от друга, означает, что два одинаковых блока в результате дадут одинаковый шифртекст. И значит, по шифртексту мы можем знать, что в исходном тексте были одинаковые блоки, и какие это блоки. Зная, что расстояния между ними кратны 128-битам, можно даже делать предположения о том, что там за данные зашифрованы. Если зашифровать картинку высокого разрешения, например, есть вероятность, что вы вообще не защитите информацию никак (форму объектов, хотя и несколько огрубленную, можно будет видеть даже непосредственно по шифртексту!).

                    Это не единственная атака, которая тут возможна, я не буду описывать их все. Именно для защиты от подобных атак и используются всякие усложения, типа режимов шифрования блочных шифров.

                    Так вот, пишите теперь про векторы инициализации и более продвинутые режимы CBC, XCB, OFB и прочие. Развивайте тему ;)
                      0
                      Кто-то из нас чего-то недопонял. А разве расписание ключей и процесс его получения не решают именно эту задачу?
                        +1
                        Не эту.

                        Расписание ключей — это какой ключ применять на каком раунде шифрования данного блока. Однако, для всех раундов разных блоков будет применено одно и то же расписание.
                        +1
                        Точное замечание :) Как применять AES это тема отдельной статьи. Когда я писал эту статью, то посмотрел на неё и прикинул сколько нужно будет крутить колесико мышки, чтобы прочитать всё и выкинул половину текста.

                        Я запланировал написать ещё пару-тройку статей. После них возьмусь за продолжение темы AES. Если смогу написать компактно, добавлю пример как MS Excel шифрует бинарные xls файлы.
                        +5
                        Был тут пост про AES, кто пропустил — вот наглядная флешка.
                        www.cs.bc.edu/~straubin/cs381-05/blockciphers/rijndael_ingles2004.swf
                          0
                          Круто!
                            0
                            Эту анимацию надо показать министру образования.
                            0
                            На мой взгляд, AES шикарен не тем, что он прост, а тем, что он изначально оптимизирован под современные 8-битные архитектуры
                              0
                              8-битные? а такие остались еще? :)
                              я немного ембеддедом занимаюсь, и все с чем работал уже давно 32битное…
                                0
                                у твоего ембеда на msm уже сиськиполгига оперативки выросло.
                                  0
                                  а чего же не 31-битное? Как была кратность 8 бит, так и осталась. Как были команды доступа к байтам и словам, так и остались.
                                0
                                ну, звиняйте. msm эт только частный случай. я и под mips'ы уже пишу (и зачем я роутер блин купил), и под ppc.
                                про армы вообще молчу ))

                                  0
                                  Спасибо! Для общего развития почитать не помешает.
                                    0
                                    осознал свою матем. неграмотность. ну не показался мне алгоритм простым(
                                      0
                                      Если не влезать в алгебру, то особо сложного тут нет.

                                      Но вообще понять, как работает финитное поле, стоит. Оно не только в шифровании используется. Другое характерное применение этого же поля — это алгоритм, на котором основана устойчивость RAID6.
                                      +3
                                      Только он, все-таки, Rijndael
                                        0
                                        Спасибо за статью. Правда не математику тяжело продвинуться дальше

                                        > У сложения есть простые свойства:
                                        > a + a = 0
                                        > -a = 0 — a = a
                                        > a — b = a + (-b) = a + b

                                        Как такое возможно если a и b не равны нулю?
                                          0
                                          Бинарное же поле.

                                          a, b in {0, 1}

                                          1 + 1 == 0
                                          =>
                                          -1 == 1

                                          -0 == 0 (очевидно)

                                          => -x == x

                                          => a — b == a + (-b) == a + b
                                            +3
                                            Точнее правильнее назвать не «бинарное», а по модулю 2
                                              0
                                              Спасибо.
                                                0
                                                Вот только смысла в таком свойстве немного. a — b == a + (-b) == 0. Для любого поля. А в бинарном это утверждение еще и очень легко проверить. Да и вообще с описанием поля автор налажал. Технически операции показаны верно, но вот понять что такое поле из этого материала невозможно.
                                                  +1
                                                  Так,… извиняюсь. Конец рабочего дня и с операцией я все перепутал. Все верно было в комментариях выше.
                                              0
                                              спасибо за статью.

                                              Как уже было отмечено, кроме простейшего режима шифрования ECB, есть другие, более безопасные режимы, которые меньше поддаются анализу — CBC, OFB, CFB, CTR.

                                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                              Самое читаемое