# Regla: Seguridad en Swift / iOS

Esta regla es OBLIGATORIA para todo código Swift en iOS.
iOS tiene mecanismos de seguridad de primera clase — el error más común es no usarlos
y recurrir a soluciones inseguras por conveniencia (UserDefaults para tokens, HTTP sin ATS).

---

## Keychain Services para secretos: nunca UserDefaults

`UserDefaults` es un plist sin cifrado. Cualquier proceso con acceso al sandbox
puede leerlo. Los secretos (tokens, credenciales, claves privadas) van en Keychain.

```swift
// MAL — token en texto plano
UserDefaults.standard.set(accessToken, forKey: "access_token")

// BIEN — Keychain con accesibilidad adecuada
import Security

struct KeychainManager {
    static func guardar(_ valor: String, para clave: String) throws {
        let data = Data(valor.utf8)
        let query: [String: Any] = [
            kSecClass as String:              kSecClassGenericPassword,
            kSecAttrAccount as String:        clave,
            kSecValueData as String:          data,
            kSecAttrAccessible as String:     kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
        ]
        SecItemDelete(query as CFDictionary)
        let estado = SecItemAdd(query as CFDictionary, nil)
        guard estado == errSecSuccess else {
            throw KeychainError.escrituraFallida(estado)
        }
    }

    static func obtener(_ clave: String) throws -> String? {
        let query: [String: Any] = [
            kSecClass as String:              kSecClassGenericPassword,
            kSecAttrAccount as String:        clave,
            kSecReturnData as String:         true,
            kSecMatchLimit as String:         kSecMatchLimitOne,
        ]
        var resultado: CFTypeRef?
        let estado = SecItemCopyMatching(query as CFDictionary, &resultado)
        guard estado == errSecSuccess, let data = resultado as? Data else { return nil }
        return String(data: data, encoding: .utf8)
    }
}
```

Usar `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` para datos que no deben
sincronizarse con iCloud Keychain ni estar disponibles cuando el dispositivo está bloqueado.

---

## App Transport Security (ATS) siempre habilitado

ATS fuerza conexiones HTTPS. No desactivarlo en producción. Nunca.

```xml
<!-- Info.plist — CORRECTO: ATS habilitado por defecto -->
<!-- No se necesita ningún key de NSAppTransportSecurity para el caso normal -->

<!-- MAL — desactivar ATS completamente -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

<!-- ACEPTABLE — excepción para un dominio específico con justificación documentada -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>legacy-api.empresa.com</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
        </dict>
    </dict>
</dict>
```

Si se necesita una excepción para la App Store, Apple requiere justificación.
Documentar en el ADR por qué el dominio no puede usar HTTPS.

---

## Certificate Pinning con URLSession

```swift
// BIEN — delegate de URLSession para validar el certificado
final class PinningURLSessionDelegate: NSObject, URLSessionDelegate {
    private let hashesEsperados: Set<String>

    init(hashesEsperados: Set<String>) {
        self.hashesEsperados = hashesEsperados
    }

    func urlSession(
        _ session: URLSession,
        didReceive challenge: URLAuthenticationChallenge,
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
    ) {
        guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
              let serverTrust = challenge.protectionSpace.serverTrust,
              let certificado = SecTrustGetCertificateAtIndex(serverTrust, 0) else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }

        let dataPublicKey = SecCertificateCopyData(certificado) as Data
        let hashActual = SHA256.hash(data: dataPublicKey)
            .map { String(format: "%02x", $0) }.joined()

        if hashesEsperados.contains(hashActual) {
            completionHandler(.useCredential, URLCredential(trust: serverTrust))
        } else {
            completionHandler(.cancelAuthenticationChallenge, nil)
        }
    }
}

// Uso
let session = URLSession(
    configuration: .default,
    delegate: PinningURLSessionDelegate(hashesEsperados: ["abc123...", "def456..."]),
    delegateQueue: nil
)
```

Incluir siempre al menos dos hashes (certificado actual + respaldo) para permitir
rotación sin dejar la app sin servicio.

---

## Data Protection API para archivos sensibles

```swift
// BIEN — archivos con protección máxima: solo accesibles cuando desbloqueado
func guardarArchivoSeguro(datos: Data, nombre: String) throws {
    let url = FileManager.default
        .urls(for: .documentDirectory, in: .userDomainMask)[0]
        .appendingPathComponent(nombre)

    try datos.write(to: url, options: [.completeFileProtection])
    // .completeFileProtection = solo accesible con dispositivo desbloqueado
    // .completeFileProtectionUnlessOpen = accesible mientras el archivo está abierto
}
```

Verificar en la entitlements que `Data Protection` está en `NSFileProtectionComplete`.

---

## Autenticación biométrica con LocalAuthentication

```swift
import LocalAuthentication

func autenticarConBiometria() async -> Bool {
    let contexto = LAContext()
    var error: NSError?

    guard contexto.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
        // Biometría no disponible — ofrecer PIN como alternativa
        return await autenticarConPIN()
    }

    do {
        return try await contexto.evaluatePolicy(
            .deviceOwnerAuthenticationWithBiometrics,
            localizedReason: "Confirma tu identidad para continuar"
        )
    } catch {
        // No loguear el tipo de error LAError — puede revelar información de intentos fallidos
        return false
    }
}
```

Usar `.deviceOwnerAuthentication` (biometría O PIN) en lugar de solo biometría
para garantizar que el usuario siempre puede autenticarse aunque la biometría falle.

---

## No hardcodear API keys ni secretos

```swift
// MAL — la key queda en el binario y es extraíble con strings o Hopper
let apiKey = "sk-prod-abc123def456"
let urlBase = "https://api.servicio.com?key=\(apiKey)"

// BIEN — la key se obtiene del servidor tras autenticacion del usuario
// El cliente recibe un token de sesión, el servidor maneja las API keys de terceros
struct ConfiguracionRemota {
    var endpointApiGateway: URL
    // Sin API keys de terceros aquí
}
```

Para SDKs que requieren clave en el cliente (ej: Stripe publishable key, Firebase):
- Usar Xcode Build Settings con `xcconfig` no versionado para distintos entornos
- Nunca en el código fuente directamente

---

## Validación de deep links y universal links

```swift
// BIEN — validar scheme, host y parámetros antes de procesar
func application(
    _ app: UIApplication,
    open url: URL,
    options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
    guard url.scheme == "miapp",
          url.host == "pedidos",
          let componentes = URLComponents(url: url, resolvingAgainstBaseURL: false),
          let parametroId = componentes.queryItems?.first(where: { $0.name == "id" })?.value,
          let pedidoId = UUID(uuidString: parametroId) else {
        // Log del intento inválido para auditoría, pero sin detalles del URL
        logger.warning("Deep link inválido recibido")
        return false
    }

    navegarADetallePedido(id: pedidoId)
    return true
}
```

Registrar intentos de deep links inválidos para detectar intentos de explotación.
NUNCA usar los parámetros del URL directamente sin validar el tipo y el formato.

---

## Checklist de seguridad Swift antes de abrir PR

- [ ] Sin secretos ni tokens en `UserDefaults` — usar Keychain
- [ ] ATS habilitado: sin `NSAllowsArbitraryLoads` en producción
- [ ] Certificate pinning configurado con hash de respaldo
- [ ] Archivos sensibles con `Data Protection (.completeFileProtection)`
- [ ] Autenticación biométrica usa `LocalAuthentication` framework
- [ ] Sin API keys hardcodeadas en el código fuente
- [ ] Deep links y universal links validados en scheme, host y parámetros
- [ ] Code signing con provisioning profiles correctos para el ambiente
