Подводные камни system.security.cryptography

71

Upload: vladimir-kochetkov

Post on 17-Jan-2017

1.500 views

Category:

Technology


0 download

TRANSCRIPT

Page 1: Подводные камни System.Security.Cryptography
Page 2: Подводные камни System.Security.Cryptography

Подводные камниSystem.Security.Cryptography;

Владимир КочетковCompilable Applications Analyzers Development / Team Lead

Positive Technologies

Page 3: Подводные камни System.Security.Cryptography

Когда и какую криптографию использовать?

3

Page 4: Подводные камни System.Security.Cryptography

2 правила самодельной криптографии

4

Никаких самодельных алгоритмов!

Никаких самодельных реализаций!

Page 5: Подводные камни System.Security.Cryptography

Задача: сравнить два байтовых массива

5

Page 6: Подводные камни System.Security.Cryptography

1. public static bool Equals(byte[] a1, byte[] a2)

2. {if (a1.Length != a2.Length)

3. {

4. return false;

5. }

for (var i=0; i < first.Length; i++)

6. {

7. if (a1[i] != a2[i])

8. {

9. return false;

10. }

11. }

12. return true;

13.}

Очевидный подход

6

Page 7: Подводные камни System.Security.Cryptography

1. public static bool Equals(byte[] a1, byte[] a2)

2. {if (a1.Length != a2.Length)

3. {

4. return false;

5. }

for (var i=0; i < first.Length; i++)

6. {

7. if (a1[i] != a2[i])

8. {

9. return false;

10. }

11. }

12. return true;

13.}

Очевидный подход

Если длины массивов не совпадут, метод завершится быстрее

Время выполнения метода линейно возрастает с ростом длины общего префикса у a1 и a2

7

Page 8: Подводные камни System.Security.Cryptography

― LAN: разница в 200 наносекунд за 1000 измерений;

― Internet: разница в 30 микросекунд за 1000 измерений;

― side-channel amplification: всегда есть возможность усилить канал.

http://www.cs.rice.edu/~dwallach/pub/crosby-timing2009.pdf

Добавление случайных временных задержек –не работает!

https://events.ccc.de/congress/2012/Fahrplan/attachments/2235_29c3-schinzel.pdf

Timing-атаки

8

Page 9: Подводные камни System.Security.Cryptography

1. public static bool Equals(byte[] a1, byte[] a2)

2. {

3. var result = true;

4. for (var i = 0; i < a1.Length; ++i)

5. {

6. if (a1[i] != a2[i])

7. {

8. result = false;

9. }

10. }

11. return result;

12. }

Попытка #2 (.NET BCL-alike)

9

Page 10: Подводные камни System.Security.Cryptography

1. public static bool Equals(byte[] a1, byte[] a2)

2. {

3. var result = true;

4. for (var i = 0; i < a1.Length; ++i)

5. {

6. if (a1[i] != a2[i])

7. {

8. result = false;

9. }

10. }

11. return result;

12. }

Попытка #2 (.NET BCL-alike)

Нет гарантии вмешательства в поток выполнения оптимизаторов C# или JIT компиляторов

10

Page 11: Подводные камни System.Security.Cryptography

1. public static bool Equals(byte[] a1, byte[] a2)

2. {

3. var diff = (uint)a.Length ^ (uint)b.Length;

4. for (var i = 0; i < a1.Length && i < a2.Length; i++)

5. {

6. diff |= (uint)(a1[i] ^ a2[i]);

7. }

8. return diff == 0;

9. }

Правильный подход

11

Page 12: Подводные камни System.Security.Cryptography

Как насчет выполнения ещедесятка других правил?

https://cryptocoding.net/index.php/Coding_rules

12

Page 13: Подводные камни System.Security.Cryptography

13

Page 14: Подводные камни System.Security.Cryptography

Годный план

Необходимо использовать:

― TLS для шифрования каналов передачи информации;

― существующие высокоуровневые библиотеки для всего остального:

• inferno (http://securitydriven.net/inferno/);

• libsodium-net (https://github.com/adamcaudill/libsodium-net);

• keyczar-dotnet (https://github.com/jbtule/keyczar-dotnet).

И только если это невозможно, следует использовать примитивы System.Security.Cryptography

14

Page 15: Подводные камни System.Security.Cryptography

Схема именования классов

― …Managed – полностью управляемая реализация;

― …CryptoServiceProvider – обертка над CryptoAPI;

― …Cng – обертка над CryptoAPI New Generation.

15

Page 16: Подводные камни System.Security.Cryptography

Схема именования классов

― …Managed – полностью управляемая реализация;

― …CryptoServiceProvider – обертка над CryptoAPI;

― …Cng – обертка над CryptoAPI New Generation.

== различные реализации одних и тех же проблем16

Page 17: Подводные камни System.Security.Cryptography

Нельзя просто так взять и использовать примитивы System.Security.Cryptography

17

Page 18: Подводные камни System.Security.Cryptography

Задача: обеспечить целостность передаваемого шифротекста

18

Page 19: Подводные камни System.Security.Cryptography

Решение: рассчитывать подпись по формуле signature = HASH(secret + ciphertext) и передавать ее вместе с шифротекстом

19

Page 20: Подводные камни System.Security.Cryptography

Низкоуровневая криптография1. private static bool IsValidSignature(string data, string signature)

2. {

3. var bytes = Encoding.ASCII.GetBytes("eCTR4rhYQVNwn78j" + data);

4. var hash = MD5.Create().ComputeHash(bytes);

5. return BitConverter.ToString(hash) == signature;

6. }

7. ...

8. if (IsValidSignature(Request["data"], Request["signature"]))

9. {

10. var decryptor = Aes.Create()

11. {

12. BlockSize = 128;

13. Key = Encoding.ASCII.GetBytes("YtGDn6mvAHbp5X7C");

14. IV = Encoding.ASCII.GetBytes("mHMUYSjiVxo4wp9R");

15. }.CreateDecryptor();

16.}20

Page 21: Подводные камни System.Security.Cryptography

Низкоуровневая криптография1. private static bool IsValidSignature(string data, string signature)

2. {

3. var bytes = Encoding.ASCII.GetBytes("eCTR4rhYQVNwn78j" + data);

4. var hash = MD5.Create().ComputeHash(bytes);

5. return BitConverter.ToString(hash) == signature;

6. }

7. ...

8. if (IsValidSignature(Request["data"], Request["signature"]))

9. {

10. var decryptor = Aes.Create()

11. {

12. BlockSize = 128;

13. Key = Encoding.ASCII.GetBytes("YtGDn6mvAHbp5X7C");

14. IV = Encoding.ASCII.GetBytes("mHMUYSjiVxo4wp9R");

15. }.CreateDecryptor();

16.}21

Жестко заданный в коде секрет

Page 22: Подводные камни System.Security.Cryptography

Низкоуровневая криптография1. private static bool IsValidSignature(string data, string signature)

2. {

3. var bytes = Encoding.ASCII.GetBytes("eCTR4rhYQVNwn78j" + data);

4. var hash = MD5.Create().ComputeHash(bytes);

5. return BitConverter.ToString(hash) == signature;

6. }

7. ...

8. if (IsValidSignature(Request["data"], Request["signature"]))

9. {

10. var decryptor = Aes.Create()

11. {

12. BlockSize = 128;

13. Key = Encoding.ASCII.GetBytes("YtGDn6mvAHbp5X7C");

14. IV = Encoding.ASCII.GetBytes("mHMUYSjiVxo4wp9R");

15. }.CreateDecryptor();

16.}22

Уязвимость к атакам удлинением сообщения

Page 23: Подводные камни System.Security.Cryptography

Низкоуровневая криптография1. private static bool IsValidSignature(string data, string signature)

2. {

3. var bytes = Encoding.ASCII.GetBytes("eCTR4rhYQVNwn78j" + data);

4. var hash = MD5.Create().ComputeHash(bytes);

5. return BitConverter.ToString(hash) == signature;

6. }

7. ...

8. if (IsValidSignature(Request["data"], Request["signature"]))

9. {

10. var decryptor = Aes.Create()

11. {

12. BlockSize = 128;

13. Key = Encoding.ASCII.GetBytes("YtGDn6mvAHbp5X7C");

14. IV = Encoding.ASCII.GetBytes("mHMUYSjiVxo4wp9R");

15. }.CreateDecryptor();

16.}23

Кто сказал, что data будет в ASCII?

Page 24: Подводные камни System.Security.Cryptography

Низкоуровневая криптография1. private static bool IsValidSignature(string data, string signature)

2. {

3. var bytes = Encoding.ASCII.GetBytes("eCTR4rhYQVNwn78j" + data);

4. var hash = MD5.Create().ComputeHash(bytes);

5. return BitConverter.ToString(hash) == signature;

6. }

7. ...

8. if (IsValidSignature(Request["data"], Request["signature"]))

9. {

10. var decryptor = Aes.Create()

11. {

12. BlockSize = 128;

13. Key = Encoding.ASCII.GetBytes("YtGDn6mvAHbp5X7C");

14. IV = Encoding.ASCII.GetBytes("mHMUYSjiVxo4wp9R");

15. }.CreateDecryptor();

16.}24

Использование уязвимой функции хэширования

Page 25: Подводные камни System.Security.Cryptography

Низкоуровневая криптография1. private static bool IsValidSignature(string data, string signature)

2. {

3. var bytes = Encoding.ASCII.GetBytes("eCTR4rhYQVNwn78j" + data);

4. var hash = MD5.Create().ComputeHash(bytes);

5. return BitConverter.ToString(hash) == signature;

6. }

7. ...

8. if (IsValidSignature(Request["data"], Request["signature"]))

9. {

10. var decryptor = Aes.Create()

11. {

12. BlockSize = 128;

13. Key = Encoding.ASCII.GetBytes("YtGDn6mvAHbp5X7C");

14. IV = Encoding.ASCII.GetBytes("mHMUYSjiVxo4wp9R");

15. }.CreateDecryptor();

16.}25

Несбалансированное сравнение строк

Page 26: Подводные камни System.Security.Cryptography

Низкоуровневая криптография1. private static bool IsValidSignature(string data, string signature)

2. {

3. var bytes = Encoding.ASCII.GetBytes("eCTR4rhYQVNwn78j" + data);

4. var hash = MD5.Create().ComputeHash(bytes);

5. return BitConverter.ToString(hash) == signature;

6. }

7. ...

8. if (IsValidSignature(Request["data"], Request["signature"]))

9. {

10. var decryptor = Aes.Create()

11. {

12. BlockSize = 128;

13. Key = Encoding.ASCII.GetBytes("YtGDn6mvAHbp5X7C");

14. IV = Encoding.ASCII.GetBytes("mHMUYSjiVxo4wp9R");

15. }.CreateDecryptor();

16.}26

AES-CBC-PKCS7 → уязвимость к атакам на оракул дополнения

Page 27: Подводные камни System.Security.Cryptography

Низкоуровневая криптография1. private static bool IsValidSignature(string data, string signature)

2. {

3. var bytes = Encoding.ASCII.GetBytes("eCTR4rhYQVNwn78j" + data);

4. var hash = MD5.Create().ComputeHash(bytes);

5. return BitConverter.ToString(hash) == signature;

6. }

7. ...

8. if (IsValidSignature(Request["data"], Request["signature"]))

9. {

10. var decryptor = Aes.Create()

11. {

12. BlockSize = 128;

13. Key = Encoding.ASCII.GetBytes("YtGDn6mvAHbp5X7C");

14. IV = Encoding.ASCII.GetBytes("mHMUYSjiVxo4wp9R");

15. }.CreateDecryptor();

16.}27

Жестко заданный в коде ключ

Page 28: Подводные камни System.Security.Cryptography

Низкоуровневая криптография1. private static bool IsValidSignature(string data, string signature)

2. {

3. var bytes = Encoding.ASCII.GetBytes("eCTR4rhYQVNwn78j" + data);

4. var hash = MD5.Create().ComputeHash(bytes);

5. return BitConverter.ToString(hash) == signature;

6. }

7. ...

8. if (IsValidSignature(Request["data"], Request["signature"]))

9. {

10. var decryptor = Aes.Create()

11. {

12. BlockSize = 128;

13. Key = Encoding.ASCII.GetBytes("YtGDn6mvAHbp5X7C");

14. IV = Encoding.ASCII.GetBytes("mHMUYSjiVxo4wp9R");

15. }.CreateDecryptor();

16.}28

Жестко заданный в коде вектор инициализации

Page 29: Подводные камни System.Security.Cryptography

Высокоуровневая криптография (почувствуйте разницу)

29

Page 30: Подводные камни System.Security.Cryptography

Высокоуровневая криптография1. // inferno

2. decryptedData = Decrypt(key, Request["data"], nonce);

30

Page 31: Подводные камни System.Security.Cryptography

Высокоуровневая криптография1. // inferno

2. decryptedData = Decrypt(key, Request["data"], nonce);

3. // keyczar-dotnet

4. using(var crypter = new Crypter(keySet))

5. {

6. decryptedData = crypter.Decrypt(Request["data"]);

7. }

31

Page 32: Подводные камни System.Security.Cryptography

Высокоуровневая криптография1. // inferno

2. decryptedData = Decrypt(key, Request["data"], nonce);

3. // keyczar-dotnet

4. using(var crypter = new Crypter(keySet))

5. {

6. decryptedData = crypter.Decrypt(Request["data"]);

7. }

8. // libsodium-net

9. decryptedData = SecretBox.Open(Request["data"], nonce, key);

32

Page 33: Подводные камни System.Security.Cryptography

Высокоуровневая криптография1. // inferno

2. decryptedData = Decrypt(key, Request["data"], nonce);

3. // keyczar-dotnet

4. using(var crypter = new Crypter(keySet))

5. {

6. decryptedData = crypter.Decrypt(Request["data"]);

7. }

8. // libsodium-net

9. decryptedData = SecretBox.Open(Request["data"], nonce, key);

33

Page 34: Подводные камни System.Security.Cryptography

Генерация случайных чисел

34

Page 35: Подводные камни System.Security.Cryptography

Типичные сценарии использования PRNG

• Генерация:

—параметров криптографических функций (векторов инициализации, оказий, значений соли, больших простых чисел и т.п.);

—идентификаторов (фрагментов файловых путей, URL);

—примитивов аутентификации (сессионных маркеров, временных паролей).

• Выбор ветви в рабочем потоке приложения.

• Специфика логики предметной области (моделирование, численный анализ).

35

Page 36: Подводные камни System.Security.Cryptography

А нужен ли вообще криптографический PRNG?

36

Page 37: Подводные камни System.Security.Cryptography

Что, если атакующий воспроизведет предыдущие илипоследующие значения, случайной последовательности?

37

Page 38: Подводные камни System.Security.Cryptography

System.Guid не предназначен для генерации случайных чисел

38

Page 39: Подводные камни System.Security.Cryptography

System.Guid

• «Черный ящик» для генерации уникальных идентификаторов → не гарантирует случайность генерируемых GUID;

• обертка над CoCreateGuid (ole32.dll) → вызов UuidCreate (rpcrt4.dll), основанный на RC4 и уязвимый к атакам на угадывание очередного значения (https://rsdn.ru/article/Crypto/UuidCrypto.xml) в ряде сценариев.

39

Константное маскирование бит снижает энтропию

Page 40: Подводные камни System.Security.Cryptography

Использование System.Randomможет обернуться серьезной уязвимостью

40

Page 41: Подводные камни System.Security.Cryptography

System.Random

• Значение seed по умолчанию Environment.TickCount → гонки за seed;

• cубтрактивный LFG(Xn = (Xn-55 - Xn-24) mod (231 - 1), n ≥ 0) → для синхронизации с генератором достаточно получить последовательность из 55 случайных чисел;

• ошибка в реализации во всех версиях .NET вплоть до текущей → неполный цикл случайной последовательности.

41

G(x) = x55 + x21 + 1 вместо x55 + x24 + 1

Page 42: Подводные камни System.Security.Cryptography

Seed-гонка

- состояние приложения, при котором PRNG в различных потоках получают одно и то же значение seed.

Эксперимент Neohapsis (http://labs.neohapsis.com/tag/seed-racing-attack-vector):

• 67 тысяч запросов на восстановление пароля;

• временный пароль генерировался с помощью System.Random;

• получено 208 ответов с уникальными паролями;

• 322 ответа содержали одинаковые пароли.42

Уникальные Одинаковые

Page 43: Подводные камни System.Security.Cryptography

Для генерации случайных чисел следует использовать наследниковRandomNumberGenerator

43

Page 44: Подводные камни System.Security.Cryptography

RandomNumberGenerator

• Абстрактный класс, в .NET BCL всего один наследник;

• RNGCryptoServiceProvider – обертка над CryptGetRandom (advapi32.dll) → FIPS-186-2 G=SHA1 (до Windows Vista SP1) и AES-CTR (все последующие версии);

• Не требуется задавать seed для генератора Crypto API;

• Опасный метод GetNonZeroBytes снижает энтропию последовательности (после 14 нулевых бит подряд всегда будет идти единичный бит) и не должен использоваться без необходимости;

44

Page 45: Подводные камни System.Security.Cryptography

Хеширование

45

Page 46: Подводные камни System.Security.Cryptography

HashAlgorithm

― KeyedHashAlgorithm:

• наследники HMAC?: OK (с учетом безопасности базовых хэш-функций);

• MACTripleDES: только для обратной совместимости.

― наследники MD5: не должны использоваться!

― наследники RIPEMD160: только для обратной совместимости

― наследники SHA-1: не должны использоваться!

― наследники SHA256/384/512: OK (пока)

46

Page 47: Подводные камни System.Security.Cryptography

Задача: обеспечить целостность передаваемых данных

47

Page 48: Подводные камни System.Security.Cryptography

Решение: рассчитывать подпись по формуле signature = HASH(secret + data) и передавать ее вместе с данными

48

Page 49: Подводные камни System.Security.Cryptography

Проблема: при таком решении атакующий сможет подделывать подпись data || attacker-data для большинства хэш-функций из .NET BCL

49

Page 50: Подводные камни System.Security.Cryptography

Атака удлинением сообщения

― Подвержен ряд хэш-функций, основанных на структуре Меркла-Дамгарда:

• MD5;

• RIPEMD-160;

• SHA-1

• SHA-256;

• SHA-512.

― Не подверждены:

• SHA-384;

• HMAC-?;

• MACT-ripleDES.

50

Page 51: Подводные камни System.Security.Cryptography

Атака удлинением сообщения

51

Изначальное хэшируемое сообщение:

m' = m || padding

Атакующему достаточно рассчитать хэш для:

m'' = m || padding || new-data || new-padding

Page 52: Подводные камни System.Security.Cryptography

Используй HMAC, Люк!

52

Атака удлинением сообщения

Page 53: Подводные камни System.Security.Cryptography

Хранение паролей

53

Page 54: Подводные камни System.Security.Cryptography

Хранение паролей1. public static string GetPasswordHash(string password)

2. {

3. Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, 32);

4. rfc2898DeriveBytes.IterationCount = 16384;

5. byte[] hash = rfc2898DeriveBytes.GetBytes(32);

6. byte[] salt = rfc2898DeriveBytes.Salt;

7. return Convert.ToBase64String(salt) + "|" + Convert.ToBase64String(hash);

8. }

Криптографические функции хэширования не подходят для хэшированияпаролей. Для этого следует использовать PBKDF2 (класс Rfc2898DeriveBytes).

Длина соли должна быть достаточной для обеспечения энтропии E=log2(nm) ≥

128 бит для любого пароля, допускаемого приложением.

54

Page 55: Подводные камни System.Security.Cryptography

Блочные шифры

55

Page 56: Подводные камни System.Security.Cryptography

SymmetricAlgorithm

― Aes: (блок 128 бит, ключ 128/192/256 бит)

― DES: не должен использоваться!

― RC2: только для обратной совместимости

― Rijndael: (блок 128/192/256 бит, ключ 128/192/256 бит)

― TripleDES: только для обратной совместимости

Чтобы использовать Rijndael как Aes, необходимо установить размер блока 128 бит и не использовать режим CFB (либо использовать feedback == 128 бит).

56

Page 57: Подводные камни System.Security.Cryptography

Критичные параметры блочных шифров

― IV – вектор инициализации:

• должен генерироваться случайным образом:

—SymmetricAlgorithm.GenerateKey();

• не должен использоваться повторно;

• может не являться секретом.

― Key – ключ шифрования:

• должен генерироваться случайным образом:

—SymmetricAlgorithm.GenerateKey();

—Rfc2898DeriveBytes (password, salt).CryptDeriveKey(…).

57

Page 58: Подводные камни System.Security.Cryptography

Критичные параметры блочных шифров

― CipherMode - режим шифрования

• CipherMode.ECB:только в качестве базы для реализации неподдерживаемых режимов!

• CipherMode.CBC:

OK, при условии ручной реализации EtA.

• CipherMode.CFB:

• CipherMode.OFB:

• CipherMode.CTS:

обеспечивают конфиденциальность шифротекста, основаны на IV, имитируют потоковое шифрование, допускают (хотя и не требуют) дополнение.

58

не поддерживаются ни одним блочным шифром

Page 59: Подводные камни System.Security.Cryptography

CipherMode.ECB

― CipherMode - режим шифрования

• CipherMode.ECB:только в качестве базы для реализации неподдерживаемых режимов!

• CipherMode.CBC:

OK, при условии ручной реализации EtA.

• CipherMode.CFB:

• CipherMode.OFB:

• CipherMode.CTS:

обеспечивают конфиденциальность шифротекста, основаны на IV, имитируют потоковое шифрование, допускают (хотя и не требуют) дополнение.

59

не поддерживаются ни одним блочным шифром

Page 60: Подводные камни System.Security.Cryptography

Критичные параметры блочных шифров

― CipherMode - режим шифрования

• CipherMode.ECB:только в качестве базы для реализации неподдерживаемых режимов!

• CipherMode.CBC:

OK, при условии ручной реализации EtA.

• CipherMode.CFB:

• CipherMode.OFB:

• CipherMode.CTS:

обеспечивают конфиденциальность шифротекста, основаны на IV, имитируют потоковое шифрование, допускают (хотя и не требуют) дополнение.

60

не поддерживаются ни одним блочным шифром

Page 61: Подводные камни System.Security.Cryptography

Критичные параметры блочных шрифтов

― PaddingMode - режим дополнения блоков:

Для данных [FF FF FF FF FF FF FF FF FF]:

• PaddingMode.ANSIX923: [FF FF FF FF FF FF FF FF FF 00 00 00 00 00 00 07]

• PaddingMode.ISO10126: [FF FF FF FF FF FF FF FF FF 7D 2A 75 EF F8 EF 07]

• PaddingMode.None: [FF FF FF FF FF FF FF FF FF]

• PaddingMode.PKCS7: [FF FF FF FF FF FF FF FF FF 07 07 07 07 07 07 07]

• PaddingMode.Zeros: [FF FF FF FF FF FF FF FF FF 00 00 00 00 00 00 00]61

Page 62: Подводные камни System.Security.Cryptography

По умолчанию все блочные шифры используют CBC-PKCS7

62

Page 63: Подводные камни System.Security.Cryptography

Проблема: если атакующий CBC-PKCS7 сможет манипулировать шифротекстом, отслеживая ошибки дополнения…

63

Page 64: Подводные камни System.Security.Cryptography

…то он сможет расшифроватьсообщение или зашифроватьпроизвольный текст, не владея ключом*

64

*за исключением первого блока

Page 65: Подводные камни System.Security.Cryptography

65

{DEMO}

Page 66: Подводные камни System.Security.Cryptography

Уязвим не только AES-CBC-PKCS7!!!

66

Page 67: Подводные камни System.Security.Cryptography

Атаки на оракул дополнения

― Уязвим любой блочный шифр.

― Уязвимы все доступные режимы дополнения, кроме PaddingMode.ISO10126 и PaddingMode.None.

― {Частично} уязвимы все доступные режимы конфиденциального шифрования, допускающие дополнение и устанавливающие обратную связь по шифротекстуили ключевому потоку между всеми блоками (CBC, OFB, CFB, CTR, …).

― Перехват исключений и добавление временных задержек не устраняют уязвимость.

67

Page 68: Подводные камни System.Security.Cryptography

Подход EtA (encrypt-then-authenticate)

1. Сообщение шифруется обычным образом (например, AES-CBC).

2. IV добавляется к шифротексту.

3. Для полученного на шаге 2 считается MAC с помощью одного из наследников HMAC (например, HMACSHA512) и также добавляется к нему.

4. …

5. Только PROFIT!!! и никаких оракулов.

68

Page 69: Подводные камни System.Security.Cryptography

EtA всегда необходимо реализовывать для любого блочного шифра System.Security.Cryptography

69

Page 70: Подводные камни System.Security.Cryptography

Вопросы?

Владимир Кочетков

[email protected]

@kochetkov_v

Compilable Applications Analyzers Development / Team LeadPositive Technologies

Page 71: Подводные камни System.Security.Cryptography