En la parte 1 guardamos secretos pequeños en el Keychain. Pero la mayoría de los datos de una app no son secretos pequeños: son archivos. Una base de datos SQLite con el historial del usuario, imágenes descargadas, un JSON con el perfil, un PDF que el usuario abrió. Todo eso vive en el disco, y mucha gente asume que iOS lo cifra todo por igual. No es así.
Qué cifra iOS y cuándo
iOS cifra el almacenamiento del dispositivo. Eso es cierto y está siempre activo mientras haya código de desbloqueo. Pero “el disco está cifrado” no significa “tus archivos son inaccesibles”. La pregunta que importa es otra: ¿cuándo puede el sistema descifrar un archivo?
Ahí entra Data Protection. Cada archivo tiene una clase de protección que decide en qué momentos su clave de cifrado está disponible. Un archivo puede estar cifrado en el disco y aun así ser legible en cuanto el dispositivo arranca, o solo cuando el usuario lo desbloquea, según la clase que le asignes.
La distinción es la que separa “cifrado” de “protegido”. El disco cifrado te defiende si alguien extrae el chip de memoria. Data Protection te defiende en el caso realista: un dispositivo bloqueado, encendido, en manos de alguien que no debería leerlo.
Las clases de protección
Son cuatro, de la más estricta a la más laxa:
- Complete (
.complete): el archivo solo es legible con el dispositivo desbloqueado. En cuanto se bloquea, la clave se descarta de memoria y el archivo queda inaccesible hasta el siguiente desbloqueo. - CompleteUnlessOpen (
.completeUnlessOpen): como la anterior, pero si el archivo ya estaba abierto cuando se bloqueó el dispositivo, sigue accesible. Útil para una descarga que continúa en segundo plano. - CompleteUntilFirstUserAuthentication (
.completeUntilFirstUserAuthentication): el archivo es legible desde el primer desbloqueo tras encender el dispositivo, y sigue siéndolo aunque se vuelva a bloquear. Es el valor por defecto en iOS. - None (
.none): el archivo es legible siempre que el dispositivo esté encendido, incluso antes del primer desbloqueo. Sin protección real más allá del cifrado de disco.
El default, CompleteUntilFirstUserAuthentication, es un compromiso razonable: protege contra un ataque en frío (dispositivo apagado que se enciende), pero no contra un dispositivo bloqueado que lleva encendido todo el día. Para datos de verdad sensibles, ese default no basta.
Subir la protección de un archivo
Cuando escribes datos, puedes pedir la clase más estricta directamente:
import Foundation
func guardarSensible(_ data: Data, en url: URL) throws {
try data.write(to: url, options: [.completeFileProtection])
}
.completeFileProtection corresponde a la clase Complete: ese archivo será ilegible con el dispositivo bloqueado. Si más tarde tu app intenta leerlo en segundo plano con la pantalla apagada, la lectura falla, y eso es exactamente lo que quieres para un dato que no debería tocarse sin el usuario presente.
Para un archivo que ya existe, o para elegir una clase intermedia, se ajusta con los atributos del FileManager:
func protegerArchivo(en url: URL) throws {
try FileManager.default.setAttributes(
[.protectionKey: FileProtectionType.complete],
ofItemAtPath: url.path
)
}
FileProtectionType tiene un caso por cada clase: .complete, .completeUnlessOpen, .completeUntilFirstUserAuthentication y .none. Elegir es cuestión de responder cuándo necesitas leer el archivo. Si solo lo tocas con la app en primer plano, .complete. Si necesitas leerlo en segundo plano con la pantalla bloqueada, no te queda otra que bajar a .completeUntilFirstUserAuthentication, y entonces conviene preguntarse si ese dato debería estar en el disco.
El caso de una base de datos
Aquí es donde la teoría se vuelve incómoda. Si guardas datos en SQLite, Core Data o SwiftData, el archivo de la base de datos hereda una clase de protección, y por defecto suele ser el compromiso intermedio. Un archivo de base de datos marcado como accesible tras el primer desbloqueo es legible durante todo el día aunque el dispositivo esté bloqueado.
Para Core Data puedes fijar la protección al configurar el store:
let descripcion = NSPersistentStoreDescription(url: urlDelStore)
descripcion.setOption(
FileProtectionType.complete as NSObject,
forKey: NSPersistentStoreFileProtectionKey
)
Con .complete, la base entera queda protegida cuando se bloquea el dispositivo. El precio es real: si la app hace trabajo en segundo plano que toca la base con la pantalla bloqueada, ese trabajo falla. No hay clase que te dé “protegido cuando bloqueado” y “accesible en segundo plano” a la vez, porque son objetivos contradictorios. Elegir es parte del trabajo.
Dónde suele haber fugas
Tres lugares donde los datos se escapan de la protección que crees haber puesto.
El primero es la carpeta temporal. Los archivos que escribes en tmp/ o en caches pueden quedar con protección mínima. Si descargas un adjunto sensible a un archivo temporal para mostrarlo, ese temporal puede ser más legible que el original.
El segundo son los thumbnails y previsualizaciones. Si tu app genera una miniatura de un documento sensible y la cachea sin proteger, la protección del documento original no sirve de nada: la copia insegura ya está en el disco.
El tercero es todo lo que sale de la app. Un archivo compartido por el share sheet, exportado a Archivos, o copiado al portapapeles, deja de estar bajo tu control y bajo tu clase de protección. La protección solo cubre lo que vive en tu contenedor.
Lo que llevamos
Resumido:
- El cifrado de disco siempre está activo, pero no es lo mismo que proteger tus archivos del acceso con el dispositivo bloqueado.
- El default (
CompleteUntilFirstUserAuthentication) deja los datos legibles todo el día tras el primer desbloqueo. - Para datos sensibles, escribe con
.completeFileProtectiono fija.completeen el store de tu base de datos. - Revisa las fugas: temporales, caches de miniaturas y todo lo que exportas fuera del contenedor.
Qué sigue
Hasta aquí protegimos datos que el usuario genera y que viven en su dispositivo. Pero hay otra clase de secreto que muchos devs meten en la app sin pensar: las claves de API. Y ahí el problema es distinto, porque no importa cuánto cifres algo que va dentro del binario: si se ejecuta en el dispositivo del usuario, se puede extraer.
En la parte 3 vemos por qué ningún secreto embebido en la app está a salvo, qué hacer con las claves de terceros que sí tienes que enviar, y dónde va de verdad un secreto que debe seguir siendo secreto.
Fuentes: Encrypting your app’s files (Apple) · FileProtectionType (Apple)