Статьи

Защита данных iOS в состоянии покоя: защита данных пользователя

Это первая из трех статей о защите пользовательских данных в состоянии покоя. В этой статье мы начнем с основ защиты данных на iOS, чтобы вы могли ознакомиться с современными рекомендациями по безопасному хранению данных с помощью Swift.

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

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

Для многих версий iOS требовалось запрашивать у приложения разрешения на использование и хранение некоторых личных данных пользователя, которые являются внешними по отношению к приложению, например, при сохранении и загрузке изображений в библиотеку фотографий. Начиная с iOS 10, любые API, которые обращаются к личным данным пользователя, требуют, чтобы вы объявили этот доступ заранее в файле info.plist вашего проекта.

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

  • Совместное использование Bluetooth: NSBluetoothPeripheralUsageDescription
  • Календарь: NSCalendarsUsageDescription
  • CallKit: NSVoIPUsageDescription
  • Камера: NSCameraUsageDescription
  • Контакты: NSContactsUsageDescription
  • Здоровье: NSHealthShareUsageDescription , NSHealthUpdateUsageDescription
  • HomeKit: NSHomeKitUsageDescription
  • Расположение: NSLocationAlwaysUsageDescription , NSLocationUsageDescription , NSLocationWhenInUseUsageDescription
  • Медиатека: NSAppleMusicUsageDescription
  • Микрофон: NSMicrophoneUsageDescription
  • Движение: NSMotionUsageDescription
  • Фотографии: NSPhotoLibraryUsageDescription
  • Напоминания: NSRemindersUsageDescription
  • Распознавание речи: NSSpeechRecognitionUsageDescription
  • SiriKit: NSSiriUsageDescription
  • Поставщик ТВ: NSVideoSubscriberAccountUsageDescription

Например, здесь есть запись в info.plist, позволяющая вашему приложению загружать и сохранять значения в календаре.

1
2
<key>NSCalendarsUsageDescription</key>
<string>View and add events to your calendar</string>

Если описание использования отсутствует, когда API пытается получить доступ к данным, приложение просто рухнет.

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

Но если вам нужно хранить данные, рекомендуется включить защиту данных Apple.

Защита данных шифрует содержимое контейнера вашего приложения. Он полагается на пользователя, имеющего пароль, и, следовательно, безопасность шифрования зависит от надежности пароля. Благодаря Touch ID и обновленному шифрованию файловой системы, представленному в iOS 10.3, система защиты данных получила много улучшений. Вы можете включить защиту данных в своем приложении, включив защиту данных в разделе « Возможности » своего файла проекта. Это обновляет ваш профиль обеспечения и файл разрешений, чтобы включить возможность защиты данных. Защита данных предлагает четыре уровня защиты, представленные структурой FileProtectionType :

  • none : нет защиты
  • complete : данные недоступны, пока устройство заблокировано. Это рекомендуемая настройка для большинства приложений.
  • completeUnlessOpen : данные доступны, когда устройство разблокировано, и остаются доступными, пока файл не будет закрыт, даже если пользователь заблокирует устройство. Файлы также могут быть созданы, когда устройство заблокировано. Эта опция хороша, когда вам нужно открыть файл для обработки и продолжить процесс, даже если пользователь переводит приложение в фоновый режим и блокирует устройство. Примером может быть задание, которое загружает файл на сервер.
  • completeUntilFirstUserAuthentication : при загрузке устройства файлы становятся недоступными до тех пор, пока пользователь впервые не разблокирует устройство. После этого файлы становятся доступными, даже если устройство снова заблокировано. Эта опция подходит для файлов, к которым необходимо получить доступ через некоторое время в фоновом режиме, когда устройство заблокировано, например, во время фонового задания.

complete — уровень по умолчанию. Чтобы избежать сбоев, когда ваш код пытается получить доступ к заблокированным данным, вы можете зарегистрироваться для получения уведомлений с помощью UIApplicationProtectedDataDidBecomeAvailable и UIApplicationProtectedDataWillBecomeUnavailable чтобы узнать, когда эти данные доступны.

1
2
3
4
5
6
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 класса FileManager .

1
2
3
4
5
6
7
8
9
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 есть метод, который может записывать свои данные в файл, и вы можете установить уровень защиты при вызове этого метода.

01
02
03
04
05
06
07
08
09
10
let data = Data.init()
let fileURL = try!
do
{
    try data.write(to: fileURL, options: ([.atomic, .completeFileProtection]))
}
catch
{
    print(error)
}

Вы также можете установить уровень защиты при настройке базовой модели данных.

01
02
03
04
05
06
07
08
09
10
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)
}

Чтобы изменить уровень защиты существующего файла, используйте следующее:

1
2
3
4
5
6
7
8
do
{
    try FileManager.default.setAttributes([FileAttributeKey.protectionKey : FileProtectionType.complete], ofItemAtPath: path)
}
catch
{
    print(error)
}

Частью защиты ваших хранимых данных является проверка их целостности. Не рекомендуется слепо доверять данным, которые вы загружаете из хранилища; это могло быть случайно или злонамеренно изменено. Протокол NSSecureCoding может использоваться для безопасной загрузки и сохранения объектов данных из хранилища. Это позволит убедиться, что загружаемые вами объекты содержат ожидаемые данные. Если вы будете сохранять свой собственный объект, вы можете соответствовать протоколу безопасного кодирования внутри вашего класса.

1
2
3
4
class ArchiveExample : NSObject, NSSecureCoding
{
    var stringExample : String?
    …

Класс должен быть унаследован от NSObject . Затем, чтобы включить безопасное кодирование, переопределите метод протокола supportSecureCoding.

1
2
3
4
5
6
7
static var supportsSecureCoding : Bool
{
    get
    {
        return true
    }
}

Если ваш пользовательский объект десериализован с помощью init?(coder aDecoder: NSCoder) , метод decodeObject(forKey:) должен быть заменен на decodeObject(of:forKey:) , который гарантирует, что правильные типы объектов будут распакованы из хранилища.

1
2
3
4
5
6
7
8
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 для загрузки данных из хранилища, убедитесь, что установлено его свойство NSKeyedUnarchiver .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class func loadFromSavedData() -> ArchiveExample?
{
    var object : ArchiveExample?
    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;
}

Включение безопасного кодирования для операций сохранения предотвратит случайное архивирование объекта, который не соответствует протоколу безопасного кодирования.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
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, где ваши скопированные данные из картона автоматически передаются между устройствами. Опять же, будьте осторожны, чтобы не подвергать конфиденциальные данные в монтажном столе Handoff. Вы можете сделать это, пометив конфиденциальный контент как localOnly . Вы также можете установить дату и время истечения срока действия данных.

01
02
03
04
05
06
07
08
09
10
11
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 в файле.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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)
}

Анимация, которая возникает при помещении приложения в фоновый режим, достигается тем, что iOS делает снимок экрана вашего приложения, которое затем используется для анимации. Когда вы смотрите на список открытых приложений на переключателе приложений, этот скриншот также используется там. Снимок экрана сохраняется на устройстве.

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

1
2
NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: .UIApplicationDidEnterBackground, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: .UIApplicationWillEnterForeground, object: nil)

Удалите ваши уведомления, когда вид исчезнет.

1
2
NotificationCenter.default.removeObserver(self, name: .UIApplicationDidEnterBackground, object: nil)
NotificationCenter.default.removeObserver(self, name: .UIApplicationWillEnterForeground, object: nil)

Ваше приложение также имеет кэш клавиатуры для текстовых полей, для которых включена автокоррекция. Текст, который вводит пользователь, вместе с недавно выученными словами, хранятся в кеше, чтобы можно было извлечь различные слова, которые пользователь ранее ввел в ваше приложение. Единственный способ отключить кэш клавиатуры — отключить функцию автокоррекции.

1
textField.autocorrectionType = UITextAutocorrectionType.no

Вы должны пометить поля пароля как безопасный ввод текста. Безопасные текстовые поля не отображают пароль и не используют кэш клавиатуры.

1
textField.isSecureTextEntry = true

Журналы отладки сохраняются в файл и могут быть получены для производственных сборок вашего приложения. Даже когда вы кодируете и отлаживаете свое приложение, не регистрируйте конфиденциальную информацию, такую ​​как пароли и ключи, на консоли. Вы можете забыть удалить эту информацию из журналов перед отправкой кода в магазин приложений! Во время отладки безопаснее использовать точку останова для просмотра конфиденциальных переменных.

Сетевые подключения также могут кэшироваться в хранилище. Более подробную информацию об удалении и отключении сетевого кэша можно найти в статье « Безопасность связи на iOS» .

Возможно, вы уже знаете, что когда файл на компьютере удаляется, часто сам файл не удаляется; удаляется только ссылка на файл. Чтобы действительно удалить файл, вы можете перезаписать файл случайными данными перед его удалением.

Переход на твердотельные накопители затруднил гарантию уничтожения данных, и лучший способ безопасного удаления данных открыт для обсуждения. Однако это руководство не будет полным без примера того, как стереть данные из хранилища. Из-за некоторых других споров об оптимизаторе Swift, а также потому, что мы надеемся гарантировать, что каждый байт файла будет перезаписан, мы реализуем эту функцию в C.

Реализация ниже может идти внутри .c файла. Вам нужно будет добавить определение функции или файл, который содержит функцию, в заголовок моста, чтобы использовать функцию из Swift. Затем вы можете вызывать эту функцию непосредственно перед FileManager местами, где вы используете FileManager removeFile . Возможно, вы захотите применить лучшие практики, описанные в этом и последующих учебниках по обновлению приложения. Затем вы можете стереть предыдущие незащищенные данные во время миграции.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#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 SDK
    Защита связи на iOS
    Коллин Штюрт
  • Мобильная разработка
    Back-End как сервис для мобильных приложений
    Сандамаль Сирипати
  • iOS SDK
    Правильный способ поделиться состоянием между контроллерами Swift View
    Маттео Манфердини
  • стриж
    Swift From Scratch: Закрытие
    Барт Джейкобс