Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/Providers/NativeServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Providers;

use Codingwithrk\NativephpSocialAuth\NativephpSocialAuthServiceProvider;
use Devnull\IosDataProtection\DataProtectionServiceProvider;
use Illuminate\Support\ServiceProvider;
use Native\Mobile\Providers\BrowserServiceProvider;
use Native\Mobile\Providers\CameraServiceProvider;
Expand Down Expand Up @@ -52,6 +53,7 @@ public function plugins(): array
ShareServiceProvider::class,
MobileSplashscreenServiceProvider::class,
ToastKitServiceProvider::class,
DataProtectionServiceProvider::class,
...(config('devnull.apple_sign_in') ? [NativephpSocialAuthServiceProvider::class] : []),
];
}
Expand Down
5 changes: 5 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
"type": "vcs",
"url": "https://github.com/simonhamp/toastkit"
},
{
"type": "path",
"url": "packages/devnull/ios-data-protection"
},
{
"name": "nativephp-plugins",
"type": "composer",
Expand All @@ -32,6 +36,7 @@
"require": {
"php": "^8.3",
"codingwithrk/nativephp-social-auth": "^1.0",
"devnull/ios-data-protection": "^1.0",
"laravel/boost": "^2.0",
"laravel/framework": "^13.0",
"laravel/tinker": "^3.0",
Expand Down
38 changes: 37 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions packages/devnull/ios-data-protection/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# devnull/ios-data-protection

A NativePHP Mobile plugin, iOS only, that protects what the app keeps on the phone:

- **Out of backups.** At every launch, `protectAppData()` marks `database/` and `storage/` under Application Support with
`isExcludedFromBackup`, so the SQLite database and Laravel's storage never reach iCloud or Finder backups.
- **Locked away while the phone is locked.** The `com.apple.developer.default-data-protection` entitlement is set to
`NSFileProtectionCompleteUnlessOpen`: files are unreadable once the phone locks, except those the app still has open,
so a write in progress finishes. Files written before the entitlement keep their old protection, so the first launch
with the plugin raises them once.

It has no PHP API. Register `DataProtectionServiceProvider` in the app's `NativeServiceProvider::plugins()` and rebuild.

Nothing here is specific to devnull: any NativePHP app that keeps private data in its database could use it, and it
could move into NativePHP itself.
26 changes: 26 additions & 0 deletions packages/devnull/ios-data-protection/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "devnull/ios-data-protection",
"description": "Keeps the app's database and storage out of iCloud and Finder backups, and unreadable while the phone is locked.",
"version": "1.0.0",
"type": "nativephp-plugin",
"license": "MIT",
"require": {
"php": "^8.3",
"nativephp/mobile": "^4.0"
},
"autoload": {
"psr-4": {
"Devnull\\IosDataProtection\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"Devnull\\IosDataProtection\\DataProtectionServiceProvider"
]
},
"nativephp": {
"manifest": "nativephp.json"
}
}
}
20 changes: 20 additions & 0 deletions packages/devnull/ios-data-protection/nativephp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "devnull/ios-data-protection",
"version": "1.0.0",
"description": "Keeps the app's database and storage out of iCloud and Finder backups, and unreadable while the phone is locked.",
"namespace": "DataProtection",
"category": "security",
"license": "MIT",
"platforms": [
"ios"
],
"bridge_functions": [],
"ios": {
"init_function": "protectAppData",
"entitlements": {
"com.apple.developer.default-data-protection": "NSFileProtectionCompleteUnlessOpen"
}
},
"events": [],
"service_provider": "Devnull\\IosDataProtection\\DataProtectionServiceProvider"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import Foundation

/// The folders under Application Support that hold what devnull sent and what
/// you haven't sent yet: the SQLite database (cached answers, Remotes messages,
/// drafts, the outbox) and Laravel's storage (cached images, files).
private let protectedFolders = ["database", "storage"]

/// Set once every file written before this plugin has its protection raised.
private let upgradedKey = "devnull.dataProtection.upgraded"

/// Called by NativePHP at every launch, before PHP starts (see nativephp.json).
///
/// They are kept out of iCloud and Finder backups: what devnull sent can be
/// fetched again, and a phone restored from a backup signs in again anyway, as
/// its key stays in this phone's Keychain. Unsent drafts don't follow you to
/// another phone. The entitlement makes files written
/// from now on unreadable once the phone locks, unless the app still has them
/// open, which a write in progress needs. Files written before it keep their
/// old protection, so they are raised once here.
func protectAppData() {
guard let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
return
}

let upgrade = !UserDefaults.standard.bool(forKey: upgradedKey)

for name in protectedFolders {
var folder = support.appendingPathComponent(name, isDirectory: true)

do {
try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)

var values = URLResourceValues()
values.isExcludedFromBackup = true
try folder.setResourceValues(values)

if upgrade {
raiseProtection(of: folder)
}
} catch {
NSLog("[DataProtection] \(name): \(error.localizedDescription)")
}
}

if upgrade {
UserDefaults.standard.set(true, forKey: upgradedKey)
}
}

private func raiseProtection(of folder: URL) {
let attributes: [FileAttributeKey: Any] = [.protectionKey: FileProtectionType.completeUnlessOpen]

try? FileManager.default.setAttributes(attributes, ofItemAtPath: folder.path)

guard let files = FileManager.default.enumerator(at: folder, includingPropertiesForKeys: nil) else {
return
}

for case let file as URL in files {
try? FileManager.default.setAttributes(attributes, ofItemAtPath: file.path)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Devnull\IosDataProtection;

use Illuminate\Support\ServiceProvider;

/**
* Registers the plugin with NativePHP. Its work is all native: the
* entitlement in nativephp.json and protectAppData() at launch.
*/
class DataProtectionServiceProvider extends ServiceProvider {}
14 changes: 14 additions & 0 deletions tests/Feature/DataProtectionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

use Native\Mobile\Plugins\PluginRegistry;

it('builds the iOS app with its data out of backups and locked away while the phone is locked', function () {
$registry = app(PluginRegistry::class);
$plugin = $registry->find('devnull/ios-data-protection');

expect($plugin)->not->toBeNull()
->and($registry->isRegistered('devnull/ios-data-protection'))->toBeTrue()
->and($plugin->getIosEntitlements())->toBe(['com.apple.developer.default-data-protection' => 'NSFileProtectionCompleteUnlessOpen'])
->and($plugin->getIosInitFunction())->toBe('protectAppData')
->and(file_get_contents($plugin->path.'/resources/ios/DataProtection.swift'))->toContain('func protectAppData()');
});
Loading