Russian (Pусский) translation by Yuri Yuriev (you can also view the original English article)
Это первая из трёх статей по защите пользовательских данных в состоянии покоя. Начнём с основ безопасности в iOS, чтобы ознакомиться с современными рекомендациями по их хранению со Swift.
Любое приложение, которое хранит данные пользователя, должно заботиться об их безопасности и секретности. Возможные нарушения могут иметь очень серьезные последствия, как мы видели по некоторым примерам. В этом уроке даны рекомендации по защите данных пользователей.
Права доступа
Прежде, чем мы займемся настройками хранения данных, давайте взглянем на те, которые могут совместно использоваться системными приложениями.
Для многих версий iOS требовалось, чтобы приложения запрашивали разрешения на использование и хранение личных данных, которые являются внешними по отношению к приложению, например при сохранении и загрузке изображений в библиотеку. Начиная с iOS 10, любые API, которые обращаются к личным данным, требуют, чтобы вы объявили об этом заранее в файле info.plist .
Много фреймворков могут обращаться к данным вне вашего приложения и у каждого свой ключ доступа.
- Bluetooth Sharing:
NSBluetoothPeripheralUsageDescription
- Calendar:
NSCalendarsUsageDescription
- CallKit:
NSVoIPUsageDescription
- Camera:
NSCameraUsageDescription
- Contacts:
NSContactsUsageDescription
- Health:
NSHealthShareUsageDescription
,NSHealthUpdateUsageDescription
- HomeKit:
NSHomeKitUsageDescription
- Location:
NSLocationAlwaysUsageDescription
,NSLocationUsageDescription
,NSLocationWhenInUseUsageDescription
- Media Library:
NSAppleMusicUsageDescription
- Microphone:
NSMicrophoneUsageDescription
- Motion:
NSMotionUsageDescription
- Photos:
NSPhotoLibraryUsageDescription
- Reminders:
NSRemindersUsageDescription
- SpeechRecognition:
NSSpeechRecognitionUsageDescription
- SiriKit:
NSSiriUsageDescription
- TV Provider:
NSVideoSubscriberAccountUsageDescription
Например, вот запись в info.plist, которая позволяет приложению загружать и хранить значения в календаре.
<key>NSCalendarsUsageDescription</key> <string>View and add events to your calendar</string>
Если при попытке API получить доступ к данным отсутствует описание, приложение завершит работу.
API защиты данных
Во-первых, нужно определиться, какую информацию и какие данные необходимо хранить в приложении. Важные данные сохраняйте в рабочей памяти, а не в хранилище файлов. Это особенно важно для любой личной информации.
Но если вам необходимо хранить данные, рекомендуется включить Apple's Data Protection.
Data Protection шифрует содержимое вашего приложения. Он полагается на код доступа, значит, безопасность шифрования связана с надёжностью пароля. С улучшением Touch ID и шифрования файловой системы, введенным в iOS 10.3, система защиты данных стала намного надёжней. Вы можете включить защиту данных в своем приложении, включив Data Protection в Capabilities файла проекта. Обновится профиль обеспечения и файл разрешений, чтобы запустить Data Protection. Защита данных предлагает четыре уровня защиты, описываемых структурой FileProtectionType
:
-
none
: нет защиты. -
complete
: данные недоступны, пока девайс заблокирован. Рекомендованная настройка для всех приложений.
-
completeUnlessOpen
: данные доступны, когда устройство разблокировано и останутся доступны, пока файл не будет закрыт, даже если пользователь заблокирует устройство. Файлы могут быть созданы, когда устройство заблокировано. Этот вариант хорош, когда вам нужно открыть файл для обработки и продолжить процесс, даже если приложение работает в фоновом режиме заблокированного устройства. В качестве примера может быть задание по загрузке файла на сервер.
-
completeUntilFirstUserAuthentication
: файлы недоступны, пока пользователь не разблокирует устройство. После этого файлы становятся доступными, даже когда устройство заблокировано заново. Этот вариант хорош для файлов, которые понадобятся позднее, в фоновом режиме, когда устройство заблокировано, например, во время извлечения фона.
complete
по умолчанию. Чтобы избежать сбоев, когда ваш код пытается получить доступ к заблокированным данным, вы можете зарегистрироваться для получения уведомлений через UIApplicationProtectedDataDidBecomeAvailable
иUIApplicationProtectedDataWillBecomeUnavailable
для поиска, когда данные станут доступны.
NotificationCenter.default.addObserver(forName: .UIApplicationProtectedDataDidBecomeAvailable, object: nil, queue: OperationQueue.main, using: { (notification) in //... }) NotificationCenter.default.addObserver(forName: .UIApplicationProtectedDataWillBecomeUnavailable, object: nil, queue: OperationQueue.main, using: { (notification) in //... })
Кроме того, можете проверить флажок UIApplication.shared.isProtectedDataAvailable
.
Важно иметь в виду при включении защиты данных, что если вы используете какие-либо фоновые службы, этот код может потребовать доступ к вашим данным в фоновом режиме, когда устройство заблокировано. Для таких файлов потребуется уровень защиты completeUntilFirstUserAuthentication
. Защиту каждого отдельного файла можно контролировать через класс FileManager
.
let ok = FileManager.default.createFile(atPath: somePath, contents: nil, attributes: [FileAttributeKey.protectionKey.rawValue: FileProtectionType.complete]) do { try FileManager.default.createDirectory(atPath: somePath, withIntermediateDirectories: true, attributes: [FileAttributeKey.protectionKey.rawValue: FileProtectionType.complete]) } catch { print(error) }
Можно установить уровень защиты при записи в файл. Объект Data
имеет метод, который может записывать свои данные в файл, и вы можете установить уровень защиты при вызове этого метода.
let data = Data.init() let fileURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("somedata.dat") do { try data.write(to: fileURL, options: ([.atomic, .completeFileProtection])) } catch { print(error) }
Вы также можете установить уровень защиты при настройке модели Core Data.
let storeURL = docURL?.appendingPathComponent("Model.sqlite") let storeOptions: [AnyHashable: Any] = [NSPersistentStoreFileProtectionKey: FileProtectionType.complete] do { try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: storeOptions) } catch { print(error) }
Чтобы изменить уровень защиты существующего файла, сделайте следующее:
do { try FileManager.default.setAttributes([FileAttributeKey.protectionKey : FileProtectionType.complete], ofItemAtPath: path) } catch { print(error) }
Неприкосновенность данных
Часть защиты сохранённых данных включает проверку их целостности. Не стоит слепо доверять данным, которые вы загружаете из хранилища; они могли быть случайно или злонамеренно изменены. Протокол NSSecureCoding
можно использовать для безопасной загрузки и сохранения данных из хранилища. Он удостоверится, что загружаемые объекты содержат ожидаемые данные. Если вы будете сохранять свой объект, он должен соответствовать протоколу безопасного кодирования внутри вашего класса.
class ArchiveExample : NSObject, NSSecureCoding { var stringExample : String? ...
Класс должен быть унаследован от NSObject
. Затем, чтобы включить безопасное кодирование, переопределите метод протокола supportsSecureCoding
.
static var supportsSecureCoding : Bool { get { return true } }
Если ваш объект упорядочивается с помощью init? (Coder aDecoder: NSCoder)
, метод decodeObject (forKey :)
должен быть заменен на decodeObject (of: forKey :)
, это гарантирует, что нужные типы объектов будут распакованы из хранилища.
required init?(coder aDecoder: NSCoder) { stringExample = aDecoder.decodeObject(of: NSString.self, forKey: "string_example") as String? } func encode(with aCoder: NSCoder) { aCoder.encode(stringExample, forKey:"string_example") }
Если вы используете NSKeyedUnarchiver
для загрузки данных из хранилища, обязательно установите его свойство RequireSecureCoding
.
class func loadFromSavedData() -> ArchiveExample? { var object : ArchiveExample? = nil let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String let url = NSURL(fileURLWithPath: path) let fileURL = url.appendingPathComponent("ArchiveExample.plist") if FileManager.default.fileExists(atPath: (fileURL?.path)!) { do { let data = try Data.init(contentsOf: fileURL!) let unarchiver = NSKeyedUnarchiver.init(forReadingWith: data) unarchiver.requiresSecureCoding = true object = unarchiver.decodeObject(of: ArchiveExample.self, forKey: NSKeyedArchiveRootObjectKey) unarchiver.finishDecoding() } catch { print(error) } } return object; }
Включение безопасного кодирования для ваших операций сохранения не позволит вам случайно архивировать объект, который не придерживается безопасного протокола кодирования.
func save() { let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String let url = NSURL(fileURLWithPath: path) let filePath = url.appendingPathComponent("ArchiveExample.plist")?.path let data = NSMutableData.init() let archiver = NSKeyedArchiver.init(forWritingWith: data) archiver.requiresSecureCoding = true archiver.encode(self, forKey: NSKeyedArchiveRootObjectKey) archiver.finishEncoding() let options : NSData.WritingOptions = [.atomic, .completeFileProtection] do { try data.write(toFile: filePath!, options:options) } catch { print(error) } }
Помимо NSSecureCoding
, полезно выполнять собственные проверки данных при распаковке любого архива или любого произвольного ввода в целом.
Следы данных
По мере развития iOS появляются новые функции, которые могут способствовать утечке хранимых данных. Начиная с iOS 9, вы можете проиндексировать ваш контент в поиске Spotlight, а в iOS 10 можно пользоваться виджетами, вроде Today Widget, который виден на заблокированном экране. Будьте осторожны, выставляя свой контент с этими новыми функциями. Вы можете показать больше, чем хотелось!
В iOS 10 появилась новая функция Handoff, в которой ваши скопированные данные автоматически распределяются между устройствами. Опять же, будьте осторожны с конфиденциальными данными для передачи. Вы можете пометить личный контент как localOnly
. Или установить дату и время истечения срока действия данных.
let stringToCopy = "copy me to pasteboard" let pasteboard = UIPasteboard.general if #available(iOS 10, *) { let tomorrow = Date().addingTimeInterval(60 * 60 * 24) pasteboard.setItems([[kUTTypeUTF8PlainText as String : stringToCopy]], options: [UIPasteboardOption.localOnly : true, UIPasteboardOption.expirationDate: tomorrow]) } else { pasteboard.string = stringToCopy }
Сохранённые файлы могут автоматически получать резервные копии, как в iTunes, так и в iCloud. Несмотря на то, что резервные копии могут быть зашифрованы, рекомендуется исключить конфиденциальные файлы, которым не нужно покидать устройство. Это можно сделать, установив для файла флажок IsExcludedFromBackup
.
let path: String = ... var url = URL(fileURLWithPath: path) do { var resourceValues = URLResourceValues() //or if you want to first check the flag: //var resourceValues = try url.resourceValues(forKeys: [.isExcludedFromBackupKey]) resourceValues.isExcludedFromBackup = true; try url.setResourceValues(resourceValues) } catch { print(error) }
Анимация, которая появляется при переходе приложения в фоновый режим, достигается снимком screenshot с вашего приложения, который затем используется для анимации. Когда вы смотрите на список открытых приложений, этот снимок там присутствует. Он сохраняется на устройстве.
Рекомендуется скрыть конфиденциальные данные, чтобы они не были зафиксированы на снимке экрана. Для этого настройте уведомление, когда приложение перейдет на задний план и установите свойство сокрытия элементов UI, которые вы хотите исключить. Они должны быть скрыты до захвата экрана iOS. Затем вы можете отображать элементы UI.
NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: .UIApplicationDidEnterBackground, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: .UIApplicationWillEnterForeground, object: nil)
Удалите уведомления, когда представление исчезнет.
NotificationCenter.default.removeObserver(self, name: .UIApplicationDidEnterBackground, object: nil) NotificationCenter.default.removeObserver(self, name: .UIApplicationWillEnterForeground, object: nil)
У вашего приложения имеется кэш клавиатуры для текстовых полей с автокоррекцией. Вводимый текст вместе с новыми словами хранится в кеше, чтобы можно было получить различные слова, которые были ранее использованы. Единственный способ отключить кеш-память клавиатуры - отключить параметр автокоррекции
textField.autocorrectionType = UITextAutocorrectionType.no
Вы должны настроить поле пароля для безопасного ввода текста. Защищённые текстовые поля не отображают пароль или не используют кэш клавиатуры.
textField.isSecureTextEntry = true
Журналы отладки сохраняются в файле и могут быть вскрыты. Даже когда вы кодируете и отлаживаете свое приложение, остерегайтесь записать на консоль личные данные, как пароли и ключи. Вы можете забыть удалить эту информацию из журналов перед отправкой кода в магазин приложений! В процессе отладки более безопасно использовать паузу для просмотра чувствительных переменных.
Сетевые подключения также могут кэшироваться для хранения. Дополнительную информацию об удалении и отключении сетевого кэша можно найти в статье Securing Communications on iOS.
Уничтожение данных
Возможно, вы знаете, что при удалении файла на компьютере часто сам файл не удаляется; удаляется только ссылка на файл. Чтобы удалить файл, вы можете перезаписать его со случайными данными.
Переход на твердотельные диски затруднил гарантию уничтожения данных, а лучший способ безопасного удаления неизвестен. Однако этот урок не был бы полным без примера того, как стереть данные из хранилища. Из-за некоторых споров вокруг Swift и потому, что мы надеемся гарантировать, что каждый байт файла фактически перезаписывается, мы реализуем эту функцию в C.
Реализация, приведенная ниже, может находиться внутри .c file. Чтобы использовать эту функцию в Swift, вам нужно добавить определение функции или файл, который содержит функцию, в свой header. Затем вы можете вызвать эту функцию прямо перед тем местом, где вы используете методы FileManager
removeFile
. Возможно, вы захотите использовать лучшие методы, описанные в этом и последующих уроках по обновлению приложения. Вы можете затем стереть предыдущие незащищённые данные.
#import <string.h> #import <sys/stat.h> #import <unistd.h> #import <errno.h> #import <fcntl.h> #import <stdio.h> #define MY_MIN(a, b) (((a) < (b)) ? (a) : (b)) int SecureWipeFile(const char *filePath) { int lastStatus = -1; for (int pass = 1; pass < 4; pass++) { //setup local vars int fileHandleInt = open(filePath, O_RDWR); struct stat stats; unsigned char charBuffer[1024]; //if can open file if (fileHandleInt >= 0) { //get file descriptors int result = fstat(fileHandleInt, &stats); if (result == 0) { switch (pass) { //DOD 5220.22-M implementation states that we write over with three passes first with 10101010, 01010101 and then the third with random data case 1: //write over with 10101010 memset(charBuffer, 0x55, sizeof(charBuffer)); break; case 2: //write over with 01010101 memset(charBuffer, 0xAA, sizeof(charBuffer)); break; case 3: //write over with arc4random for (unsigned long i = 0; i < sizeof(charBuffer); ++i) { charBuffer[i] = arc4random() % 255; } break; default: //at least write over with random data for (unsigned long i = 0; i < sizeof(charBuffer); ++i) { charBuffer[i] = arc4random() % 255; } break; } //get file size in bytes off_t fileSizeInBytes = stats.st_size; //rewrite every byte of the file ssize_t numberOfBytesWritten; for ( ; fileSizeInBytes; fileSizeInBytes -= numberOfBytesWritten) { //write bytes from the buffer into the file numberOfBytesWritten = write(fileHandleInt, charBuffer, MY_MIN((size_t)fileSizeInBytes, sizeof(charBuffer))); } //close the file lastStatus = close(fileHandleInt); } } } return lastStatus; }
Заключение
В этой статье вы узнали о настройке разрешений для данных, к которым имеет доступ ваше приложение, а также о том, как обеспечить защиту и целостность основных файлов. Ещё мы рассмотрели некоторые возможности утечки данных из вашего приложения. Пользователи доверяют вам защиту своих данных. Следуя этим рекомендациям, вы сможете вернуть им уверенность.
Пока что ознакомьтесь с некоторыми другими нашими статьями о разработке приложений для iOS!
- iOS SDKSecuring Communications on iOSCollin Stuart
- Mobile DevelopmentBack-End as a Service for Mobile AppsBala Durage Sandamal Siripathi
- iOS SDKThe Right Way to Share State Between Swift View ControllersMatteo Manferdini
- SwiftSwift From Scratch: ClosuresBart Jacobs