NSFileProtectionComplete doesn't encrypt the core data file
Asked Answered
X

4

7

I am using Xcode 7.3 for iOS 9.3 to try and encrypt a Core Data file. I am trying to use NSPersistentStoreFileProtectionKey and set it to NSFileProtectionComplete to enable the encryption. It is not working for some reason and I can always see the .sqlite file generated by the app and browse through the content in sqlitebrowser or iexplorer. Here is my code :

lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
    // The persistent store coordinator for the application. This implementation creates and returns a coordinator, having added the store for the application to it. This property is optional since there are legitimate error conditions that could cause the creation of the store to fail.

    // Create the coordinator and store
    let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
    let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("SingleViewCoreData.sqlite")
    var failureReason = "There was an error creating or loading the application's saved data."


    let dict: [NSObject : AnyObject] = [
        NSPersistentStoreFileProtectionKey        : NSFileProtectionComplete
    ]

    do {
        try coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: dict)
    } catch {
        // Report any error we got.
        var dict = [String: AnyObject]()
        dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data"
        dict[NSLocalizedFailureReasonErrorKey] = failureReason

        dict[NSUnderlyingErrorKey] = error as NSError
        let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
        // Replace this with code to handle the error appropriately.
        // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
        NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
        abort()
    }

    do {
        let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("SingleViewCoreData.sqlite")
        try NSFileManager.defaultManager().setAttributes([NSFileProtectionKey : NSFileProtectionComplete], ofItemAtPath: url.path!)

    } catch {

    }

    do {
        let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("SingleViewCoreData.sqlite-wal")
        try NSFileManager.defaultManager().setAttributes([NSFileProtectionKey : NSFileProtectionComplete], ofItemAtPath: url.path!)
        //            try print(NSFileManager.defaultManager().attributesOfFileSystemForPath(String(url)))

    } catch {

    }

    do {
        let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("SingleViewCoreData.sqlite-shm")
        try NSFileManager.defaultManager().setAttributes([NSFileProtectionKey : NSFileProtectionComplete], ofItemAtPath: url.path!)
        //            try print(NSFileManager.defaultManager().attributesOfFileSystemForPath(String(url)))

    } catch {

    }


    return coordinator
}()

I have also enabled Data Protection for my target in the "Capabilities". I have regenerated the provisioning profile from the Apple Developer portal and am using that with Enabled Data Protection.

I am also using the following code to check the file attributes of .sqlite , .sqlite-wal and .sqlite-shm files. NSFileProtectionKey is correctly set for all 3 of them.

func checkProtectionForLocalDb(atDir : String){

    let fileManager = NSFileManager.defaultManager()
    let enumerator: NSDirectoryEnumerator = fileManager.enumeratorAtPath(atDir)!


    for path in enumerator {

        let attr : NSDictionary = enumerator.fileAttributes!
        print(attr)


    }


}

I also tried disabling the Journal mode to prevent -wal and -shm files from being created. But I can still read the .sqlite file. Even though the attributes read NSFileProtectionComplete.

As described in the Apple Documentation at Apple Docs under "Protecting Data using On Disk Encryption", I tried to check whether the value of variable protectedDataAvailable changes as shown in the code below

public func applicationDidEnterBackground(application: UIApplication) {
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    NSThread.sleepForTimeInterval(10)
    sleep(10)
    let dataAvailable : Bool = UIApplication.sharedApplication().protectedDataAvailable
    print("Protected Data Available : " + String(dataAvailable))

}

If I check the value without the delay it's set to true but after adding the delay it's set to false. This is kind of encouraging, however, right after, when I download the container, to show the content, it still has .sqlite file that still shows the content when opened in sqlitebrowser.

Xyster answered 25/8, 2016 at 18:1 Comment(2)
How are you testing? What is the state of the app (open/closed/killed) and the device (open/locked) when you test?Constipation
I have tested the app in open/close and device locked/unlocked states. One scenario where I was hoping it would always work was when the device was locked but connected to Xcode and I downloaded the container which would come with a new timestamp and new version number but could still read the data.Xyster
M
11

Ok, I finally understand this.

Using Xcode 7.3.1

Enabling File Protection

  1. Enable File Protection using the Capabilities tab on your app target
  2. If you do not want the default NSFileProtectionComplete, change this setting in the developer portal under your app id
  3. Make sure Xcode has the new provisioning profile this creates.
  4. For protecting files your app creates, that's it.
  5. To protect Core Data, you need to add the NSPersistentStoreFileProtectionKey: NSFileProtectionComplete option to your persistent store.

Example:

var options: [NSObject : AnyObject] = [NSMigratePersistentStoresAutomaticallyOption: true,
                   NSPersistentStoreFileProtectionKey: NSFileProtectionComplete,
                NSInferMappingModelAutomaticallyOption: true]
    do {
        try coordinator!.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: options)

Testing File Protection

I am not able to test this using a non-jailbroken device connected to a computer. Every attempt to access the device this way requires that I "trust" the computer and I believe that trusted computers are always able to read the phone's data ("Trusted computers can sync with your iOS device, create backups, and access your device's photos, videos, contacts, and other content" - https://support.apple.com/en-us/HT202778). I think the other answers on SO referencing this technique are no longer valid with more recent versions of iOS. Indeed, I am always able to download the container using XCode and view the app's data using iPhone Explorer. So how to test...

1 - Create an archive and ensure that it is has the proper entitlements by running the following on the .app file from the command line:

codesign -d --entitlements :- <path_to_app_binary>

You should see a key/value pair that represents your Data Protection level. In this example, NSFileProtectionComplete:

<key>com.apple.developer.default-data-protection</key>
<string>NSFileProtectionComplete</string>

In addition, I used the following two techniques to satisfy myself that the data protection is indeed working. They both require code changes.

2 - Add some code to verify that the proper NSFileProtectionKey is being set on your files and/or core data store:

NSFileManager.defaultManager().attributesOfItemAtPath(dbPath.path!)

If I print this out on one of my files I get:

["NSFileCreationDate": 2016-10-14 02:06:39 +0000, "NSFileGroupOwnerAccountName": mobile, "NSFileType": NSFileTypeRegular, "NSFileSystemNumber": 16777218, "NSFileOwnerAccountName": mobile, "NSFileReferenceCount": 1, "NSFileModificationDate": 2016-10-14 02:06:39 +0000, "NSFileExtensionHidden": 0, "NSFileSize": 81920, "NSFileGroupOwnerAccountID": 501, "NSFileOwnerAccountID": 501, "NSFilePosixPermissions": 420, "NSFileProtectionKey": NSFileProtectionComplete, "NSFileSystemFileNumber": 270902]

Note the "NSFileProtectionKey": "NSFileProtectionComplete" pair.

3 - Modify the following code and hook it up to some button in your app.

@IBAction func settingButtonTouch(sender: AnyObject) {
        updateTimer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self,
                                                             selector: #selector(TabbedOverviewViewController.runTest), userInfo: nil, repeats: true)
        registerBackgroundTask()
}

var backgroundTask: UIBackgroundTaskIdentifier = UIBackgroundTaskInvalid
var updateTimer: NSTimer?

func registerBackgroundTask() {
    backgroundTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler {
        [unowned self] in
        self.endBackgroundTask()
    }
    assert(backgroundTask != UIBackgroundTaskInvalid)
}

func endBackgroundTask() {
    NSLog("Background task ended.")
    UIApplication.sharedApplication().endBackgroundTask(backgroundTask)
    backgroundTask = UIBackgroundTaskInvalid
}

func runTest() {
    switch UIApplication.sharedApplication().applicationState {
    case .Active:
        NSLog("App is active.")
        checkFiles()
    case .Background:
        NSLog("App is backgrounded.")
        checkFiles()
    case .Inactive:
        break
    }
}

func checkFiles() {
    // attempt to access a protected resource, i.e. a core data store or file
}        

When you tap the button this code begins executing the checkFiles method every .5 seconds. This should run indefinitely with the app in the foreground or background - until you lock your phone. At that point it should reliably fail after roughly 10 seconds - exactly as described in the description of NSFileProtectionComplete.

Mcdougal answered 14/10, 2016 at 13:49 Comment(0)
T
1

We need to understand how Data Protection works. Actually, you don't even need to enable it. Starting with iOS7, the default protection level is “File Protection Complete until first user authentication.”

This means that the files are not accessible until the user unlocks the device for the first time. After that, the files remain accessible even when the device is locked and until it shuts down or reboots.

The other thing is that you're going to see the app's data on a trusted computer always - regardless of the Data Protection level setting.

However, the data can’t be accessed if somebody tries to read them from the flash drive directly. The purpose of Data Protection is to ensure that sensitive data can’t be extracted from a password-protected device’s storage.

After running this code, I could still access and read the contents written to protectedFileURL, even after locking the device.

    do {
        try data.write(to: protectedFileURL, options: .completeFileProtectionUnlessOpen)
    } catch {
        print(error)
    }

But that's normal since I ran iExplorer on a trusted computer. And for the same reason, it's fine if you see your sqlite file.

The situation is different if your device gets lost or stolen. A hacker won't be able to read the sqlite file since it's encrypted. Well, unless he guesses your passcode somehow.

Trill answered 22/8, 2018 at 19:57 Comment(0)
I
1

Swift 5.0 & Xcode 11:

  1. Enable "Data Protection" in "Capabilities".
  2. Use the following code to protect a file or folder at a specific path:

    // Protects a file or folder + excludes it from backup.
    // - parameter path: Path component of the file.
    // - parameter fileProtectionType: `FileProtectionType`.
    // - returns: True, when protected successful.
    static func protectFileOrFolderAtPath(_ path: String, fileProtectionType: FileProtectionType) -> Bool {
        guard FileManager.default.fileExists(atPath: path) else { return false }
    
        let fileProtectionAttrs = [FileAttributeKey.protectionKey: fileProtectionType]
        do {
            try FileManager.default.setAttributes(fileProtectionAttrs, ofItemAtPath: path)
            return true
        } catch {
            assertionFailure("Failed protecting path with error: \(error).")
            return false 
        }
    }
    
  3. (Optional) Use the following code to check whether the file or folder at the specific path is protected (note: This only works on physical devices):

    /// Returns true, when the file at the provided path is protected.
    /// - parameter path: Path of the file to check.
    /// - note: Returns true, for simulators. Simulators do not have hardware file encryption. This feature is only available for real devices.
    static func isFileProtectedAtPath(_ path: String) -> Bool {
        guard !Environment.isSimulator else { return true } // file protection does not work on simulator!
        do {
            let attributes = try FileManager.default.attributesOfItem(atPath: path)
            if attributes.contains(where: { $0.key == .protectionKey }) {
                return true
            } else {
                return false
            }
        } catch {
            assertionFailure(String(describing: error))
            return false
        }
    }
    
Internist answered 24/10, 2019 at 7:57 Comment(0)
L
0

Rather than encrypt a file at the local level I set NSFileProtectionComplete for the app as a whole.

Create the file 'entitlements.plist' in your apps root folder with the following content.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>DataProtectionClass</key>
    <string>NSFileProtectionComplete</string>
</dict>
</plist>

Then if you haven't already done so already (this could be the problem with your file level encryption) enable Data Protection in your apps capabilities.

enter image description here

Lungwort answered 31/8, 2016 at 9:4 Comment(12)
As specified in my question, I have already tried enabling Data Protection from within Xcode under "Capability" section as your screenshot, but it didn't work. I also enabled Data Protection in the development provisioning profile and regenerated and installed it. None of that worked. As for the code you have pasted above for entitlement.plist, is it the same as putting it in the info.plist or should I create the new file? Also, what is your recommended method of testing the enabled encryption? I always seem to be able to open the .sqlite file even after device is locked.Xyster
@Xyster Testing: I've updated my previous answer to check the your passcode is set immediately and to include the wait time to allow the device to be encrypted (I tried this dozens of times before realising the encryption has a 10-20 second delay). https://mcmap.net/q/516273/-implementing-and-testing-ios-data-protectionLungwort
@Xyster The entitlements plist is a different entity than your info.plist. Putting the DataProtection key in info.plist did not work for me.Lungwort
I followed the steps you have mentioned in the other answer. Instead of waiting for 20 seconds, I have waited as long as 2-3 minutes, but I can still browse through the content of .sqlite file. For the entitlements.plist, I thought that is handled by the new provisioning profile where the "Data Protection" is enabled and hence that key would get added. However, I will also try adding it manually to see if that works. About testing, are you saying that the files encrypted would not even show up in the list? or the file would be there but the data would be encrypted?Xyster
@Xyster They won't show up. Enabling Data Protection in the provisioning profile alone did not work for me. I needed to do it in the Project entitlements page and within the entitlements plist.Lungwort
@ Deco I created the entitlements plist at the same path my info plist is located with the content as you suggested to set the DataProtectionClass, I can still see .sqlite file and it's content by downloading the container from Xcode after device is locked for more than a minute.Xyster
Apple documentation suggests that any changes made in the Target->Capabilities will automatically get reflected in the entitlements - developer.apple.com/library/ios/documentation/IDEs/Conceptual/… (second para. under "Adding Capabilities". I still tried it, but am not sure whether Xcode is using the newly created entitlements plist in the build process.Xyster
@EmbCoderm Running short on ideas now I'm afraid. What version of iOS are you running and is your test device set to require your passcode immediately?Lungwort
I am running on iOS 9.3.5. I really appreciate you looking into this. Btw, I also tried one more thing which I added to my question above to check in applicationDidEnterBackground whether "protectedDataAvailable" is set properly. Unfortunately, even thought that variable is set properly after 10-20 seconds of delay after device lock, still .sqlite file is available and can be browsed through! This kind of tells me that there has to be another way of testing this, as in, not by downloading the container from Xcode.Xyster
And yeah, my device is set to require passcode immediately. Sigh!!Xyster
I'm having similar problems. No matter what I do I can see my the files in iPhone Explorer and by downloading the container in XCode. My files are having their NSFileProtectionKey set appropriately so I want to believe things are set up correctly. My leading theory is that you can't test File Protection with a "trusted" computer because it has special access to your data. Of course, if that's true, I think it means you need a jailbroken device to really test File Protection.Mcdougal
I am having the similar issue as I am able to browse my sqlite file after downloading the container which is Data Protected with Complete. Did any one found the solutions on hoe to test it ? Help appreciated.Crabb

© 2022 - 2024 — McMap. All rights reserved.