Чи SecureString коли-небудь практичний у додатку C #?


224

Ви можете мене виправити, якщо мої припущення помиляються тут, але дозвольте мені пояснити, чому я прошу.

Взяте з MSDN SecureString:

Представляє текст, який слід зберігати в таємниці. Текст шифрується для конфіденційності при використанні та видаляється з пам'яті комп'ютера, коли це вже не потрібно.

Я розумію, це має повний сенс зберігати пароль або іншу приватну інформацію в більш SecureStringніж System.String, тому що ви можете контролювати, як і коли він фактично зберігається в пам'яті, оскільки System.String:

він є незмінним і, коли це більше не потрібно, не може бути запрограмований програмно для вивезення сміття; тобто екземпляр читається лише після його створення, і неможливо передбачити, коли екземпляр буде видалений з пам'яті комп'ютера. Отже, якщо об’єкт String містить конфіденційну інформацію, таку як пароль, номер кредитної картки або особисті дані, існує ризик, що інформація може бути розкрита після її використання, оскільки ваша програма не може видалити дані з пам'яті комп'ютера.

Однак у випадку програми GUI (наприклад, ssh-клієнта), SecureString необхідно будувати з а System.String . Усі текстові елементи керування використовують рядок як основний тип даних .

Отже, це означає, що кожного разу, коли користувач натискає клавішу, старий рядок, який там був, відкидається, і будується нова рядок, який представляє значення в текстовому полі, навіть якщо використовується маска пароля. І ми не можемо контролювати, коли або якщо якесь із цих значень відкидається з пам'яті .

Тепер настав час увійти на сервер. Вгадай що? Для аутентифікації потрібно пройти рядок через з'єднання . Тож давайте перетворимо наше SecureStringв System.String.... і тепер у нас є рядок на купі, що не може змусити його пройти збирання сміття (або записати 0 в його буфер).

Моя точка : незалежно від того , що ви робите, де - то вздовж лінії, що SecureStringце збирається бути перетворені в System.String, тобто це буде по крайней мере , існує на купі в якій - то момент (без будь - яких гарантій збору сміття).

Моя думка не в тому : чи існують способи обходу відправлення рядка до ssh-з'єднання, або обхід наявного контрольного сховища рядка (зробіть спеціальний елемент управління). Для цього питання ви можете замінити "ssh-з'єднання" на "форму входу", "форму реєстрації", "форму оплати", "продукти-ви-годуєте-годуєте-щеня, але не-ваші діти", тощо.

  • Отже, в який момент робить використання a SecureString фактично стає практичним?
  • Чи варто коли-небудь додаткового часу на розробку повністю усунути використання System.String об'єкта?
  • Чи є вся суть у тому, SecureStringщоб просто скоротити кількість часу, що System.Stringзнаходиться на купі (зменшуючи ризик переходу до фізичного файлу своп)?
  • Якщо у зловмисника вже є засоби для перевірки у купі, він, швидше за все, або (A) вже має засоби для читання натискань клавіш, або (B) вже фізично має автомат ... Так, якщо використовувати SecureStringзапобігання йому потрапити до дані все одно?
  • Це просто "безпека через неясність"?

Вибачте, якщо я задаю питання надто густо, цікавість просто покращила мене. Не соромтесь відповісти на будь-яке або всі мої запитання (або скажіть, що мої припущення абсолютно неправильні). :)


25
Майте на увазі, що SecureStringнасправді це не захищена рядок. Це просто спосіб зменшити вікно часу, протягом якого хтось може перевірити вашу пам'ять і успішно отримати конфіденційні дані. Це не куленепроникне, і цього не передбачалося. Але бали, які ви набираєте, дуже справедливі. Пов’язано: stackoverflow.com/questions/14449579/…
Теодорос Чатзіґянакікіс

1
@TheodorosChatzigiannakis, так, це майже все, що я зрозумів. Я щойно провів цілий день, лаяючи свій мозок, намагаючись розібратися в безпечному способі зберігання пароля протягом життя програми, і це просто змусило мене замислитися, чи варто це?
Стівен Джефріс

1
Напевно, не варто. Але потім, знову ж таки, ви можете захищатись від екстремального сценарію. Крайній не в тому сенсі, що навряд чи хтось отримає такий доступ до вашого комп’ютера, а в тому сенсі, що якщо хтось отримає такий доступ, комп'ютер вважається (для всіх намірів і цілей) компрометованим, і я не ' я думаю, що існує будь-яка мова чи будь-яка техніка, яку ви можете повністю захистити від цього.
Теодорос Чатзіґянакікіс

8
Він захищає від того, щоб пароль був видно роками , довго після того, як всі перестали думати, що може виникнути проблема. На викинутий жорсткий диск, який зберігається у файлі підкачки. Скраб пам'яті, щоб шанси на це були низькими, важливо, не можна цього робити з System.String, оскільки це незмінне. Для цього потрібна інфраструктура, до якої мало хто з програм сьогодні має доступ, взаємодіяти з нативним кодом, тому методи Marshal.SecureStringXxx () корисні стають рідкісними. Або гідний спосіб підказати користувача на це :)
Ганс Пасант

1
SecureString - це поглиблена техніка безпеки. Компроміс між вартістю розвитку та підвищенням безпеки не є сприятливим у більшості ситуацій. Для цього дуже мало випадків корисного використання.
usr

Відповіді:


237

Насправді це дуже практичне використання SecureString .

Чи знаєте ви, скільки разів я бачив такі сценарії? (відповідь: багато!):

  • Пароль у файлі журналу з'являється випадково.
  • Десь показується пароль - одного разу GUI показав командний рядок програми, що запускається, а командний рядок складався з пароля. На жаль .
  • Використання профіле пам'яті для профільного програмного забезпечення разом із колегою. Колега бачить ваш пароль у пам’яті. Звучить нереально? Зовсім ні.
  • Я колись використовував RedGate програмне забезпечення, яке могло б зафіксувати "значення" локальних змінних у випадку винятків, надзвичайно корисне. Хоча я можу собі уявити, що він введе "випадкові паролі" випадково.
  • Дамп аварійного завершення, який включає рядок пароля.

Чи знаєте ви, як уникнути всіх цих проблем? SecureString. Зазвичай це гарантує, що ви не будете робити дурних помилок як таких. Як цього уникнути? Переконайтесь, що пароль зашифрований у некерованій пам’яті, а до справжнього значення можна отримати лише тоді, коли ви на 90% впевнені, що ви робите.

В тому сенсі, SecureString працює досить легко:

1) Все зашифровано

2) Дзвінки користувача AppendChar

3) Розшифруйте все в UNMANAGED MEMORY і додайте символ

4) Зашифруйте все заново в НЕМЕНЕНЗОВАНОЙ ПАМ'ЯТІ.

Що робити, якщо користувач має доступ до вашого комп'ютера? Чи міг би вірус отримати доступ до всіх SecureStrings? Так. Все, що вам потрібно зробити, - це підключити себе RtlEncryptMemoryпід час розшифрування пам'яті, ви отримаєте місце незашифрованої адреси пам'яті та прочитаєте її. Вуаля! Насправді, ви можете зробити вірус, який буде постійно перевіряти на використання SecureStringта записувати всі дії з ним. Я не кажу, що це буде легке завдання, але це можна зробити. Як бачите, "потужність" системи SecureStringповністю зникла, коли у вашій системі є користувач / вірус.

У вас є кілька пунктів у вашому дописі. Звичайно, якщо ви використовуєте деякі елементи управління інтерфейсом, які містять "рядок пароля" внутрішньо, використовуючи фактичнийSecureString не так корисно. Хоча все-таки це може захистити від певної дурості, яку я перераховував вище.

Крім того, як зазначали інші, WPF підтримує PasswordBox, який використовує SecureStringвнутрішньо через свою властивість SecurePassword .

Суть є; якщо у вас є конфіденційні дані (паролі, кредитні картки, ..), використовуйте SecureString. Це те, за чим слід C Framework. Наприклад, NetworkCredentialклас зберігає пароль як SecureString. Якщо ви подивитеся на це , ви можете побачити понад ~ 80 різних звичаїв у .NET-рамкахSecureString .

Є багато випадків, коли вам доведеться конвертувати SecureString на рядок, оскільки деякі API очікують цього.

Звичайна проблема:

  1. API - GENERIC. Невідомо, що є чутливі дані.
  2. API знає, що він має справу з конфіденційними даними та використовує "рядок" - це просто поганий дизайн.

Ви підняли добру думку: що станеться, коли SecureStringпереходить у нього string? Це може статися лише через перший пункт. Наприклад, API не знає, що це чутливі дані. Я особисто не бачив, щоб це відбувалося. Виведення рядка з SecureString не так просто.

Це не просто з простої причини ; ніколи не було наміру дозволяти користувачу перетворювати SecureString у рядок, як ви заявили: GC запуститься. Якщо ви бачите, що це робите, вам потрібно відступити і запитати себе: навіщо я це навіть роблю, чи мені це справді потрібно це, чому?

Я бачив один цікавий випадок. А саме, функція LoAUUser WinApi приймає LPTSTR як пароль, а це означає, що вам потрібно зателефонувати SecureStringToGlobalAllocUnicode. Це в основному дає вам незашифрований пароль, який живе в некерованій пам'яті. Потрібно позбутися цього, як тільки закінчите:

// Marshal the SecureString to unmanaged memory.
IntPtr rawPassword = Marshal.SecureStringToGlobalAllocUnicode(password);
try
{
   //...snip...
}
finally 
{
   // Zero-out and free the unmanaged string reference.
   Marshal.ZeroFreeGlobalAllocUnicode(rawPassword);
}

Ви завжди можете розширити SecureStringклас за допомогою методу розширення, наприклад ToEncryptedString(__SERVER__PUBLIC_KEY), який дає вам stringекземпляр, SecureStringякий шифрується за допомогою відкритого ключа сервера. Лише сервер може розшифрувати його. Проблема вирішена: Колекція сміття ніколи не побачить "оригінальну" рядок, як ви ніколи не виставляєте її в керовану пам'ять. Це саме те, що робиться в PSRemotingCryptoHelper( EncryptSecureStringCore(SecureString secureString)).

І як щось дуже майже пов'язане: Mono SecureString взагалі не шифрується . Виконання було прокоментовано, тому що ... чекайте на це .. "Це якимось чином викликає поломку тесту nunit" , що підводить до моєї останньої точки:

SecureStringне підтримується скрізь. Якщо платформа / архітектура не підтримує SecureString, ви отримаєте виняток. Існує список платформ, які підтримуються в документації.


Я думаю , SecureStringбуло б набагато більше практичним , якби існував механізм для доступу до його окремих символів. Ефективність такого механізму може бути підвищена за рахунок має тип структури SecureStringTempBufferбез будь - яких суспільних членів, які можуть бути передані з допомогою refдо SecureString«читання один символ» метод [якщо шифрування / дешифрування обробляється в 8-символьних шматків, така структура може містити дані для 8 символів та розташування першого з цих символів у межах SecureString].
supercat

2
Я позначаю це у кожному аудиті вихідного коду, який я виконую. Також слід повторити, що якщо ви SecureStringйого використовуєте, його потрібно використовувати весь шлях.
Кейсі

1
Я реалізував розширення .ToEncryptedString (), але використовуючи сертифікат. Ви проти заглянути, і дайте мені знати, якщо я роблю це все неправильно. Я сподіваюсь на її безпечний engouhg gist.github.com/sturlath/99cfbfff26e0ebd12ea73316d0843330
Стурла

@Sturla - Що є EngineSettingsу вашому методі розширення?
InteXX


15

У ваших припущеннях кілька питань.

Насамперед, клас SecureString не має конструктора String. Для того, щоб створити такий, ви виділяєте об'єкт, а потім додаєте символи.

Що стосується GUI або консолі, ви можете дуже легко передати кожну натиснуту клавішу в захищену рядок.

Клас створений таким чином, що ви не можете помилково отримати доступ до значення, яке зберігається. Це означає, що ви не можете отриматиstring пароль як пароль безпосередньо з нього.

Тому для його використання, наприклад, для аутентифікації через Інтернет, вам доведеться використовувати належні класи, які також захищені.

У рамках .NET у вас є кілька класів, які можуть використовувати SecureString

  • Керування паролем WPF PasswordBox зберігає пароль як SecureString всередині.
  • Властивість пароля System.Diagnostics.ProcessInfo - це SecureString.
  • Конструктор для X509Certificate2 приймає SecureString для пароля.

(більше)

На закінчення, клас SecureString може бути корисним, але вимагає більшої уваги з боку розробника.

Все це, з прикладами, добре описано в документації MSDN щодо SecureString


10
Я не зовсім розумію третій до останнього абзацу вашої відповіді. Як би ви перенесли SecureStringмережу, не серіалізуючи її на string? Я думаю, що питання OP полягає в тому, що настає час, коли ви хочете фактично використовувати значення, яке зберігається в безпеці SecureString, а це означає, що ви повинні його дістати, і це вже не безпечно. У всій бібліотеці Framework Class практично немає методів, які приймають a SecureStringяк безпосередньо вхідний (наскільки я бачу), тож який сенс утримувати значення SecureStringв першу чергу?
stakx - більше не вносить дописи

@ DamianLeszczyński-Vash, я знаю, що в SecureString немає конструктора струн, але моя думка полягає в тому, що ви (A) створюєте SecureString і додаєте до нього кожну таблицю з рядка, або (B) в якийсь момент потрібно використовувати значення, збережене в SecureString як рядок, щоб передати його деякому API. Коли це говориться, ви приводите кілька хороших моментів, і я бачу, як у деяких дуже конкретних випадках це може бути корисним.
Стівен Джеффріс

1
@stakx Ви можете надіслати його по мережі, отримуючи char[]та надсилаючи кожен charпо сокету - не створюючи жодних об'єктів, що збирають сміття в процесі.
користувач253751

Char [] є колекціонованим та змінним, тому його можна перезаписати.
Пітер Річі

Звичайно, також легко забути перезаписати цей масив. У будь-якому випадку, будь-який API, який ви передаєте символам, також може зробити копію.
cHao

10

SecureString корисний, якщо:

  • Ви будуєте його за символом (наприклад, з консольного введення) або отримуєте його з некерованого API

  • Ви використовуєте його, передаючи його в некерований API (SecureStringToBSTR).

Якщо ви коли-небудь перетворюєте його в керований рядок, ви перемогли його призначення.

ОНОВЛЕННЯ у відповідь на коментар

... або BSTR, як ви згадуєте, який не здається більш захищеним

Після його перетворення в BSTR, некерований компонент, який споживає BSTR, може нульову пам'ять. Некерована пам'ять є більш захищеною в тому сенсі, що її можна скинути таким чином.

Однак у .NET Framework дуже мало API, що підтримують SecureString, тому ви маєте рацію сказати, що це сьогодні дуже обмежене значення.

Основний випадок використання, який я бачу, - це клієнтська програма, яка вимагає від користувача введення високочутливого коду чи пароля. Вхід користувача може бути використаний символом за символом для створення SecureString, тоді це може бути передано некерованому API, який нулює BSTR, який він отримує після його використання. Будь-який наступний дамп пам'яті не буде містити чутливий рядок.

У серверній програмі важко зрозуміти, де це було б корисно.

ОНОВЛЕННЯ 2

Одним із прикладів API .NET, який приймає SecureString, є цей конструктор для класу X509Certificate . Якщо ви опитуєтесь з ILSpy або подібним, ви побачите, що SecureString внутрішньо перетворюється на некерований буфер ( Marshal.SecureStringToGlobalAllocUnicode), який потім дорівнює нулю, коли закінчується з ( Marshal.ZeroFreeGlobalAllocUnicode).


1
+1, але чи не завжди ви будете "перемагати своє призначення"? З огляду на те, що майже жоден метод у бібліотеці Framework Class не приймає SecureStringяк вхідне джерело, яке б їх використання? Вам завжди доведеться конвертувати назад stringрано чи пізно (або, BSTRяк ви згадуєте, що не здається більш безпечним). То навіщо взагалі турбуватися SecureString?
stakx - більше не вносить дописів

5

Microsoft не рекомендує використовувати SecureStringновіші коди.

З документації класу SecureString :

Важливо

Ми не рекомендуємо використовувати SecureStringклас для нової розробки. Для отримання додаткової інформації див. SecureStringНе слід використовувати

Що рекомендує:

Не використовуйте SecureString для нового коду. Під час перенесення коду до .NET Core врахуйте, що вміст масиву не зашифровано в пам'яті.

Загальний підхід до роботи з обліковими записами - це уникати їх і замість цього покладатися на інші способи аутентифікації, наприклад сертифікати або автентифікацію Windows. на GitHub.


4

Як ви правильно визначили, SecureStringпропонує одну конкретну перевагу перед string: детермінованим стиранням. З цим фактом є дві проблеми:

  1. Як уже згадували інші, і як ви самі помітили, цього недостатньо саме по собі. Ви повинні переконатися, що кожен крок процесу (включаючи пошук введення, побудова рядка, використання, видалення, транспортування тощо) відбувається без пошкодження мети використання SecureString. Це означає , що ви повинні бути обережні , щоб ніколи не створити GC керованого незмінні stringабо будь-який інший буфер , який буде зберігати конфіденційну інформацію (або ви повинні стежити , що а). На практиці цього не завжди легко досягти, оскільки безліч API надають лише спосіб роботиstring , не такSecureString . І навіть якщо вам все вдасться правильно ...
  2. SecureStringзахищає від дуже специфічних видів нападу (а для деяких з них це навіть не так надійно). Наприклад, SecureStringчи дозволяє вам скоротити часове вікно, в якому зловмисник може скинути пам'ять вашого процесу та успішно витягти конфіденційну інформацію (знову, як ви правильно вказали), але сподіваючись, що вікно занадто мале, щоб зловмисник зробити знімок пам’яті зовсім не вважається безпекою.

Отже, коли ви повинні його використовувати? Тільки коли ви працюєте з чимось, що дозволяє вам працювати з SecureStringусіма вашими потребами, і навіть тоді ви повинні пам’ятати, що це безпечно лише в конкретних обставинах.


2
Ви припускаєте, що зловмисник може зробити знімок пам'яті процесу в будь-який конкретний час. Це не обов'язково так. Розглянемо сміттєвий відвал. Якщо збій стався після того, як додаток було зроблено з конфіденційними даними, дамп аварійного завершення не повинен містити конфіденційних даних.

4

Нижче текст скопійовано з аналізатора статичного коду HP Fortify

Анотація: Метод PassString () у PassGenerator.cs зберігає чутливі дані в небезпечний спосіб (тобто в рядку), що дозволяє витягувати дані за допомогою перевірки купи.

Пояснення: Чутливі дані (наприклад, паролі, номери соціального страхування, номери кредитних карток тощо), що зберігаються в пам'яті, можуть просочуватися, якщо вони зберігаються в керованому об'єкті String. Строкові об'єкти не закріплені, тому сміттєзбірник може пересунути ці об’єкти за бажанням і залишити кілька копій у пам'яті. Ці об'єкти не шифруються за замовчуванням, тому кожен, хто зможе прочитати пам'ять процесу, зможе побачити вміст. Крім того, якщо пам'ять процесу буде заміщена на диск, незашифроване вміст рядка буде записано у файл свопу. Нарешті, оскільки об'єкти String є незмінними, видалення значення String з пам'яті може здійснити лише сміттєзбірник CLR. Збір сміття не потрібно запускати, якщо CLR не має пам’яті, тому немає гарантії, коли відбудеться вивезення сміття.

Рекомендації: Замість того, щоб зберігати конфіденційні дані в таких об'єктах, як Strings, зберігайте їх у об’єкті SecureString. Кожен об’єкт постійно зберігає його вміст у зашифрованому форматі в пам'яті.


3

Я хотів би звернутися до цього питання:

Якщо у зловмисника вже є засоби для перевірки у купі, вони, швидше за все, або (A) вже мають засоби для читання натискань клавіш, або (B) вже фізично мають автомат ... Тож використовуючи SecureStringзапобігання їм потрапляти до дані все одно?

Зловмисник може не мати повного доступу до комп'ютера та програми, але може мати засоби для доступу до деяких частин пам'яті процесу. Зазвичай це викликано помилками, як перекриття буфера, коли спеціально сконструйований вхід може спричинити додаток до викриття чи перезапису певної частини пам'яті.

Серцеві кровотечі просочується пам'ять

Візьмемо для прикладу Heartbleed. Спеціально побудовані запити можуть змусити код піддавати зловмиснику випадкові частини пам'яті процесу. Зловмисник може витягнути з пам'яті сертифікати SSL, але єдине, що йому потрібно, - це лише використовувати неправильний запит.

У світі керованого коду перевищення буфера стає проблемою набагато рідше. А у випадку WinForms, дані вже зберігаються небезпечно, і ви нічого не можете з цим зробити. Це робить захист SecureStringмайже марним.

Однак графічний інтерфейс може бути запрограмований на використання SecureString, і в такому випадку зменшення вікна наявності пароля в пам'яті може бути вартим того. Наприклад, PasswordBox.SecurePassword від WPF має тип SecureString.


Хм, точно цікаво знати. Чесно кажучи, я насправді не так переживаю типи атак, необхідних навіть для отримання значення System.String з моєї пам’яті, я просто запитую з чистої цікавості. Однак, дякую за інформацію! :)
Стівен Джеффріс

2

Деякий час тому мені довелося створити інтерфейс AC # проти шлюзу платежів за допомогою кредитної картки Java, і потрібні були сумісні шифровані комунікаційні ключі. Оскільки реалізація Java була досить специфічною, і мені довелося працювати із захищеними даними певним чином.

Мені здалося, що ця конструкція є досить простою у використанні та легшою, ніж робота з SecureString… для тих, хто любить користуватися… почувайтеся вільними, ніяких юридичних обмежень :-). Зауважте, що ці класи є внутрішніми, можливо, вам доведеться оприлюднити їх.

namespace Cardinity.Infrastructure
{
    using System.Security.Cryptography;
    using System;
    enum EncryptionMethods
    {
        None=0,
        HMACSHA1,
        HMACSHA256,
        HMACSHA384,
        HMACSHA512,
        HMACMD5
    }


internal class Protected
{
    private  Byte[] salt = Guid.NewGuid().ToByteArray();

    protected byte[] Protect(byte[] data)
    {
        try
        {
            return ProtectedData.Protect(data, salt, DataProtectionScope.CurrentUser);
        }
        catch (CryptographicException)//no reason for hackers to know it failed
        {
#if DEBUG
            throw;
#else
            return null;
#endif
        }
    }

    protected byte[] Unprotect(byte[] data)
    {
        try
        {
            return ProtectedData.Unprotect(data, salt, DataProtectionScope.CurrentUser);
        }
        catch (CryptographicException)//no reason for hackers to know it failed
        {
#if DEBUG
            throw;
#else
            return null;
#endif
        }
    }
}


    internal class SecretKeySpec:Protected,IDisposable
    {
        readonly EncryptionMethods _method;

        private byte[] _secretKey;
        public SecretKeySpec(byte[] secretKey, EncryptionMethods encryptionMethod)
        {
            _secretKey = Protect(secretKey);
            _method = encryptionMethod;
        }

        public EncryptionMethods Method => _method;
        public byte[] SecretKey => Unprotect( _secretKey);

        public void Dispose()
        {
            if (_secretKey == null)
                return;
            //overwrite array memory
            for (int i = 0; i < _secretKey.Length; i++)
            {
                _secretKey[i] = 0;
            }

            //set-null
            _secretKey = null;
        }
        ~SecretKeySpec()
        {
            Dispose();
        }
    }

    internal class Mac : Protected,IDisposable
    {
        byte[] rawHmac;
        HMAC mac;
        public Mac(SecretKeySpec key, string data)
        {

            switch (key.Method)
            {
                case EncryptionMethods.HMACMD5:
                    mac = new HMACMD5(key.SecretKey);
                    break;
                case EncryptionMethods.HMACSHA512:
                    mac = new HMACSHA512(key.SecretKey);
                    break;
                case EncryptionMethods.HMACSHA384:
                    mac = new HMACSHA384(key.SecretKey);
                    break;
                case EncryptionMethods.HMACSHA256:
                    mac = new HMACSHA256(key.SecretKey);

                break;
                case EncryptionMethods.HMACSHA1:
                    mac = new HMACSHA1(key.SecretKey);
                    break;

                default:                    
                    throw new NotSupportedException("not supported HMAC");
            }
            rawHmac = Protect( mac.ComputeHash(Cardinity.ENCODING.GetBytes(data)));            

        }

        public string AsBase64()
        {
            return System.Convert.ToBase64String(Unprotect(rawHmac));
        }

        public void Dispose()
        {
            if (rawHmac != null)
            {
                //overwrite memory address
                for (int i = 0; i < rawHmac.Length; i++)
                {
                    rawHmac[i] = 0;
                }

                //release memory now
                rawHmac = null;

            }
            mac?.Dispose();
            mac = null;

        }
        ~Mac()
        {
            Dispose();
        }
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.