Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
544
.claude/skills/axiom-file-protection-ref/SKILL.md
Normal file
544
.claude/skills/axiom-file-protection-ref/SKILL.md
Normal file
@@ -0,0 +1,544 @@
|
||||
---
|
||||
name: axiom-file-protection-ref
|
||||
description: Use when asking about 'FileProtectionType', 'file encryption iOS', 'NSFileProtection', 'data protection', 'secure file storage', 'encrypt files at rest', 'complete protection', 'file security' - comprehensive reference for iOS file encryption and data protection APIs
|
||||
license: MIT
|
||||
compatibility: iOS 4.0+, iPadOS 4.0+, macOS 10.0+
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
last-updated: "2025-12-12"
|
||||
---
|
||||
|
||||
# iOS File Protection Reference
|
||||
|
||||
**Purpose**: Comprehensive reference for file encryption and data protection APIs
|
||||
**Availability**: iOS 4.0+ (all protection levels), latest enhancements in iOS 26
|
||||
**Context**: Built on iOS Data Protection architecture using hardware encryption
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when you need to:
|
||||
- Protect sensitive user data at rest
|
||||
- Choose appropriate FileProtectionType for files
|
||||
- Understand when files are accessible/encrypted
|
||||
- Debug "file not accessible" errors after device lock
|
||||
- Implement secure file storage
|
||||
- Compare Keychain vs file protection approaches
|
||||
- Handle background file access requirements
|
||||
|
||||
## Overview
|
||||
|
||||
iOS Data Protection provides **hardware-accelerated file encryption** tied to the device passcode. When a user sets a passcode, every file can be encrypted with keys protected by that passcode.
|
||||
|
||||
**Key concepts**:
|
||||
- Files are encrypted **automatically** when protection is enabled
|
||||
- Encryption keys are derived from device hardware + user passcode
|
||||
- Files become **inaccessible** when device is locked (depending on protection level)
|
||||
- No performance cost (hardware AES encryption)
|
||||
|
||||
---
|
||||
|
||||
## Protection Levels Comparison
|
||||
|
||||
| Level | Encrypted Until | Accessible When | Use For | Background Access |
|
||||
|-------|-----------------|-----------------|---------|-------------------|
|
||||
| **complete** | Device unlocked | Only while unlocked | Sensitive data (health, finances) | ❌ No |
|
||||
| **completeUnlessOpen** | File closed | After first unlock, while open | Large downloads, videos | ✅ If already open |
|
||||
| **completeUntilFirstUserAuthentication** | First unlock after boot | After first unlock | Most app data | ✅ Yes |
|
||||
| **none** | Never | Always | Public caches, temp files | ✅ Yes |
|
||||
|
||||
### Detailed Level Descriptions
|
||||
|
||||
#### .complete
|
||||
|
||||
**Full Description**:
|
||||
> "The file is stored in an encrypted format on disk and cannot be read from or written to while the device is locked or booting."
|
||||
|
||||
**Use For**:
|
||||
- User health data
|
||||
- Financial information
|
||||
- Password vaults
|
||||
- Sensitive documents
|
||||
- Personal photos (if app requires maximum security)
|
||||
|
||||
**Behavior**:
|
||||
- Encrypted: ✅ Always
|
||||
- Accessible: Only when device unlocked
|
||||
- Background access: ❌ No (app can't read while locked)
|
||||
- Available after boot: ❌ No (until user unlocks)
|
||||
|
||||
**Code Example**:
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Maximum security for sensitive data
|
||||
func saveSensitiveData(_ data: Data, to url: URL) throws {
|
||||
try data.write(to: url, options: .completeFileProtection)
|
||||
}
|
||||
|
||||
// Or set on existing file
|
||||
try FileManager.default.setAttributes(
|
||||
[.protectionKey: FileProtectionType.complete],
|
||||
ofItemAtPath: url.path
|
||||
)
|
||||
```
|
||||
|
||||
**Tradeoffs**:
|
||||
- ✅ Maximum security
|
||||
- ❌ Can't access in background
|
||||
- ❌ User sees errors if app tries to access while locked
|
||||
|
||||
#### .completeUnlessOpen
|
||||
|
||||
**Full Description**:
|
||||
> "The file is stored in an encrypted format on disk after it is closed."
|
||||
|
||||
**Use For**:
|
||||
- Large file downloads (continue in background)
|
||||
- Video files being played
|
||||
- Documents being edited
|
||||
- Any file that needs background access while open
|
||||
|
||||
**Behavior**:
|
||||
- Encrypted: ✅ When closed
|
||||
- Accessible: After first unlock, remains accessible while open
|
||||
- Background access: ✅ Yes (if file was already open)
|
||||
- Available after boot: ❌ No (until first unlock)
|
||||
|
||||
**Code Example**:
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Download in background, but encrypted when closed
|
||||
func startBackgroundDownload(url: URL, destination: URL) throws {
|
||||
try Data().write(to: destination, options: .completeFileProtectionUnlessOpen)
|
||||
|
||||
// Open file handle for writing
|
||||
let fileHandle = try FileHandle(forWritingTo: destination)
|
||||
|
||||
// Download continues in background
|
||||
// File remains accessible because it's open
|
||||
// When closed, file becomes encrypted
|
||||
|
||||
// Later, when download complete:
|
||||
try fileHandle.close() // Now encrypted until next unlock
|
||||
}
|
||||
```
|
||||
|
||||
**Tradeoffs**:
|
||||
- ✅ Good security (encrypted when not in use)
|
||||
- ✅ Background access (if already open)
|
||||
- ⚠️ Vulnerable while open
|
||||
|
||||
#### .completeUntilFirstUserAuthentication
|
||||
|
||||
**Full Description**:
|
||||
> "The file is stored in an encrypted format on disk and cannot be accessed until after the device has booted."
|
||||
|
||||
**Use For**:
|
||||
- Most application data
|
||||
- User preferences
|
||||
- Downloaded content
|
||||
- Database files
|
||||
- Anything that needs background access
|
||||
|
||||
**Behavior**:
|
||||
- Encrypted: ✅ Always
|
||||
- Accessible: After first unlock following boot
|
||||
- Background access: ✅ Yes (after first unlock)
|
||||
- Available after boot: ❌ No (until user unlocks once)
|
||||
|
||||
**This is the recommended default for most files.**
|
||||
|
||||
**Code Example**:
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Balanced security for most app data
|
||||
func saveAppData(_ data: Data, to url: URL) throws {
|
||||
try data.write(
|
||||
to: url,
|
||||
options: .completeFileProtectionUntilFirstUserAuthentication
|
||||
)
|
||||
}
|
||||
|
||||
// ✅ This file can be accessed in background after first unlock
|
||||
func backgroundTaskCanAccessFile() {
|
||||
// This works even if device is locked (after first unlock)
|
||||
let data = try? Data(contentsOf: url)
|
||||
}
|
||||
```
|
||||
|
||||
**Tradeoffs**:
|
||||
- ✅ Protected during boot (device stolen while off)
|
||||
- ✅ Background access (normal operation)
|
||||
- ⚠️ Accessible while locked (less protection than .complete)
|
||||
|
||||
#### .none
|
||||
|
||||
**Full Description**:
|
||||
> "The file has no special protections associated with it."
|
||||
|
||||
**Use For**:
|
||||
- Public cache data
|
||||
- Temporary files
|
||||
- Non-sensitive downloads
|
||||
- Thumbnails
|
||||
- Only when absolutely necessary
|
||||
|
||||
**Behavior**:
|
||||
- Encrypted: ❌ Never
|
||||
- Accessible: ✅ Always
|
||||
- Background access: ✅ Always
|
||||
- Available after boot: ✅ Always
|
||||
|
||||
**Code Example**:
|
||||
|
||||
```swift
|
||||
// ⚠️ USE SPARINGLY: Only for truly non-sensitive data
|
||||
func cachePublicThumbnail(_ data: Data, to url: URL) throws {
|
||||
try data.write(to: url, options: .noFileProtection)
|
||||
}
|
||||
```
|
||||
|
||||
**Tradeoffs**:
|
||||
- ✅ Always accessible
|
||||
- ❌ No encryption
|
||||
- ❌ Vulnerable if device is stolen
|
||||
|
||||
---
|
||||
|
||||
## Setting File Protection
|
||||
|
||||
### At File Creation
|
||||
|
||||
```swift
|
||||
// ✅ RECOMMENDED: Set protection when writing
|
||||
let sensitiveData = userData.jsonData()
|
||||
try sensitiveData.write(
|
||||
to: fileURL,
|
||||
options: .completeFileProtection
|
||||
)
|
||||
```
|
||||
|
||||
### On Existing Files
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Change protection on existing file
|
||||
try FileManager.default.setAttributes(
|
||||
[.protectionKey: FileProtectionType.complete],
|
||||
ofItemAtPath: fileURL.path
|
||||
)
|
||||
```
|
||||
|
||||
### Default Protection for Directory
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Set default protection for directory
|
||||
// New files inherit this protection
|
||||
try FileManager.default.setAttributes(
|
||||
[.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication],
|
||||
ofItemAtPath: directoryURL.path
|
||||
)
|
||||
```
|
||||
|
||||
### Checking Current Protection
|
||||
|
||||
```swift
|
||||
// ✅ Check file's current protection level
|
||||
func checkFileProtection(at url: URL) throws -> FileProtectionType? {
|
||||
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
|
||||
return attributes[.protectionKey] as? FileProtectionType
|
||||
}
|
||||
|
||||
// Usage
|
||||
if let protection = try? checkFileProtection(at: fileURL) {
|
||||
switch protection {
|
||||
case .complete:
|
||||
print("Maximum protection")
|
||||
case .completeUntilFirstUserAuthentication:
|
||||
print("Standard protection")
|
||||
default:
|
||||
print("Other protection")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Protection vs Keychain
|
||||
|
||||
### Decision Matrix
|
||||
|
||||
| Use Case | Recommended | Why |
|
||||
|----------|-------------|-----|
|
||||
| Passwords, tokens, keys | **Keychain** | Designed for small secrets |
|
||||
| Small sensitive values (<few KB) | **Keychain** | More secure, encrypted separately |
|
||||
| Files >1 KB | **File Protection** | Keychain not designed for large data |
|
||||
| User documents | **File Protection** | Natural file-based storage |
|
||||
| Structured secrets | **Keychain** | Query by key, access control |
|
||||
|
||||
### Code Comparison
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Small secrets in Keychain
|
||||
let passwordData = password.data(using: .utf8)!
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrAccount as String: "userPassword",
|
||||
kSecValueData as String: passwordData,
|
||||
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
|
||||
]
|
||||
SecItemAdd(query as CFDictionary, nil)
|
||||
|
||||
// ✅ CORRECT: Files with file protection
|
||||
let userData = try JSONEncoder().encode(user)
|
||||
try userData.write(to: fileURL, options: .completeFileProtection)
|
||||
```
|
||||
|
||||
**Keychain advantages**:
|
||||
- More granular access control (Face ID/Touch ID)
|
||||
- Separate encryption (not tied to file system)
|
||||
- Survives app deletion (if configured)
|
||||
|
||||
**File protection advantages**:
|
||||
- Works with existing file operations
|
||||
- Handles large data efficiently
|
||||
- Automatic with minimal code
|
||||
|
||||
---
|
||||
|
||||
## Background Access Considerations
|
||||
|
||||
### iOS Background Modes and File Protection
|
||||
|
||||
```swift
|
||||
// ❌ WRONG: .complete files can't be accessed in background
|
||||
class BackgroundTask {
|
||||
func performBackgroundSync() {
|
||||
// This FAILS if file has .complete protection and device is locked
|
||||
let data = try? Data(contentsOf: sensitiveFileURL)
|
||||
// data will be nil if device locked
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ CORRECT: Use .completeUntilFirstUserAuthentication
|
||||
// Files accessible in background after first unlock
|
||||
try data.write(
|
||||
to: fileURL,
|
||||
options: .completeFileProtectionUntilFirstUserAuthentication
|
||||
)
|
||||
```
|
||||
|
||||
### Handling Protection Errors
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Handle protection errors gracefully
|
||||
func readFile(at url: URL) -> Data? {
|
||||
do {
|
||||
return try Data(contentsOf: url)
|
||||
} catch let error as NSError {
|
||||
if error.domain == NSCocoaErrorDomain &&
|
||||
error.code == NSFileReadNoPermissionError {
|
||||
// File is protected and device is locked
|
||||
print("File protected, device locked")
|
||||
return nil
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## iCloud and File Protection
|
||||
|
||||
### How Protection Works with iCloud
|
||||
|
||||
**Local file protection**:
|
||||
- Applied to local cached copies
|
||||
- Does NOT affect iCloud-stored versions
|
||||
- iCloud has its own encryption (in transit and at rest)
|
||||
|
||||
**iCloud encryption**:
|
||||
- All iCloud data encrypted at rest (Apple-managed keys)
|
||||
- End-to-end encryption available for some data types (Advanced Data Protection)
|
||||
- File protection only affects local device
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Protection on iCloud file affects local copy only
|
||||
func saveToICloud(data: Data, filename: String) throws {
|
||||
guard let iCloudURL = FileManager.default.url(
|
||||
forUbiquityContainerIdentifier: nil
|
||||
) else { return }
|
||||
|
||||
let fileURL = iCloudURL.appendingPathComponent(filename)
|
||||
|
||||
// This protection applies to local cached copy
|
||||
try data.write(to: fileURL, options: .completeFileProtection)
|
||||
|
||||
// iCloud has separate encryption for cloud storage
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Default Protection for New Apps
|
||||
|
||||
```swift
|
||||
// ✅ RECOMMENDED: Set default protection at app launch
|
||||
func configureDefaultFileProtection() {
|
||||
let fileManager = FileManager.default
|
||||
|
||||
let directories: [FileManager.SearchPathDirectory] = [
|
||||
.documentDirectory,
|
||||
.applicationSupportDirectory
|
||||
]
|
||||
|
||||
for directory in directories {
|
||||
guard let url = fileManager.urls(
|
||||
for: directory,
|
||||
in: .userDomainMask
|
||||
).first else { continue }
|
||||
|
||||
try? fileManager.setAttributes(
|
||||
[.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication],
|
||||
ofItemAtPath: url.path
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Call during app initialization
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions...) {
|
||||
configureDefaultFileProtection()
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Encrypting Database Files
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Protect SwiftData/SQLite database
|
||||
let appSupportURL = FileManager.default.urls(
|
||||
for: .applicationSupportDirectory,
|
||||
in: .userDomainMask
|
||||
)[0]
|
||||
|
||||
let databaseURL = appSupportURL.appendingPathComponent("app.sqlite")
|
||||
|
||||
// Set protection before creating database
|
||||
try? FileManager.default.setAttributes(
|
||||
[.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication],
|
||||
ofItemAtPath: appSupportURL.path
|
||||
)
|
||||
|
||||
// Now create database - it inherits protection
|
||||
let container = try ModelContainer(
|
||||
for: MyModel.self,
|
||||
configurations: ModelConfiguration(url: databaseURL)
|
||||
)
|
||||
```
|
||||
|
||||
### Pattern 3: Downgrading Protection for Background Tasks
|
||||
|
||||
```swift
|
||||
// ⚠️ SOMETIMES NECESSARY: Lower protection for background access
|
||||
func enableBackgroundAccess(for url: URL) throws {
|
||||
try FileManager.default.setAttributes(
|
||||
[.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication],
|
||||
ofItemAtPath: url.path
|
||||
)
|
||||
}
|
||||
|
||||
// Only do this if:
|
||||
// 1. Background access is truly required
|
||||
// 2. Data sensitivity allows it
|
||||
// 3. You've considered security tradeoffs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging File Protection Issues
|
||||
|
||||
### Issue: File Not Accessible in Background
|
||||
|
||||
**Symptom**: Background tasks fail to read files
|
||||
|
||||
```swift
|
||||
// Debug: Check current protection
|
||||
if let protection = try? FileManager.default.attributesOfItem(
|
||||
atPath: url.path
|
||||
)[.protectionKey] as? FileProtectionType {
|
||||
print("Protection: \(protection)")
|
||||
if protection == .complete {
|
||||
print("❌ Can't access in background when locked")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Solution**: Use `.completeUntilFirstUserAuthentication` instead
|
||||
|
||||
### Issue: Files Inaccessible After Restart
|
||||
|
||||
**Symptom**: App can't access files immediately after device reboot
|
||||
|
||||
**Cause**: Using `.complete` or `.completeUntilFirstUserAuthentication` (works as designed)
|
||||
|
||||
**Solution**: This is expected behavior. Either:
|
||||
1. Wait for user to unlock device
|
||||
2. Handle gracefully with appropriate UI
|
||||
3. Use `.none` for files that must be accessible (security tradeoff)
|
||||
|
||||
---
|
||||
|
||||
## Entitlements
|
||||
|
||||
File protection generally works without special entitlements, but some features require:
|
||||
|
||||
### Data Protection Entitlement
|
||||
|
||||
```xml
|
||||
<!-- Required for: .complete protection level -->
|
||||
<key>com.apple.developer.default-data-protection</key>
|
||||
<string>NSFileProtectionComplete</string>
|
||||
```
|
||||
|
||||
**When needed**:
|
||||
- Using `.complete` protection
|
||||
- Some iOS versions for any protection (check documentation)
|
||||
|
||||
**How to add**:
|
||||
1. Xcode → Target → Signing & Capabilities
|
||||
2. "+ Capability" → Data Protection
|
||||
3. Select protection level
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference Table
|
||||
|
||||
| Scenario | Recommended Protection | Accessible When Locked? | Background Access? |
|
||||
|----------|------------------------|-------------------------|---------------------|
|
||||
| User health data | `.complete` | ❌ No | ❌ No |
|
||||
| Financial records | `.complete` | ❌ No | ❌ No |
|
||||
| Most app data | `.completeUntilFirstUserAuthentication` | ✅ Yes (after first unlock) | ✅ Yes |
|
||||
| Downloads (large files) | `.completeUnlessOpen` | ✅ While open | ✅ While open |
|
||||
| Database files | `.completeUntilFirstUserAuthentication` | ✅ Yes | ✅ Yes |
|
||||
| Downloaded images | `.completeUntilFirstUserAuthentication` | ✅ Yes | ✅ Yes |
|
||||
| Public caches | `.none` | ✅ Yes | ✅ Yes |
|
||||
| Temp files | `.none` | ✅ Yes | ✅ Yes |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `axiom-storage` — Decide when to use file protection vs other security measures
|
||||
- `axiom-storage-management-ref` — File lifecycle, purging, and disk management
|
||||
- `axiom-storage-diag` — Debug file access issues
|
||||
- `axiom-keychain` — Secure credential storage (tokens, passwords, keys)
|
||||
- `axiom-keychain-ref` — Complete SecItem API reference
|
||||
- `axiom-cryptokit` — Encryption and signing with CryptoKit
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-12
|
||||
**Skill Type**: Reference
|
||||
**Minimum iOS**: 4.0 (all protection levels)
|
||||
**Latest Updates**: iOS 26
|
||||
Reference in New Issue
Block a user