Una historia de copiar y pegar

El pasado 8 de febrero estaba escribiendo un artículo diferente cuando me vi copiando texto desde mi móvil a mi ordenador como la cosa más natural del mundo, y tuve que publicarlo en X:

«Cualquier tecnología suficientemente avanzada es indistinguible de la magia». Vale, me dedico a hacer software, y puedo entender cómo funciona el proceso, e incluso me atrevería a poder hacer algo parecido, pero que copies un texto en un iPhone y lo pegues en un Mac es MAGIA.

Eso no quedó así, ya que horas más tarde seguía pensando en cómo hacer que esto funcionara, y anuncié mis intenciones al mundo:

Pues me he liado la manta a la cabeza y estoy haciendo mi propio portapapeles entre mi móvil y mi ordenador y viceversa. Una prueba de concepto divertida y una mirada al bajo nivel. Pronto en el blog!

Y dos semanas después aquí estamos. Espero que disfrutes de este viaje hacia el fascinante mundo de los portapapeles y las conexiones entre iOS y Mac.

La pregunta

¿Cuán sencillo sería replicar esa magia? Mi idea original era que el texto que copiábamos se mandaba a un servidor, pero igual no necesitamos hacerlo tan complicado, si conseguimos que un Mac y un iPhone se hablen dentro de una red local.

Disclaimer: Mi experiencia de software a bajo nivel es muy limitada, así que es posible que esté cometiendo auténticas barbaridades, pero mi objetivo es intentar aprender algo. Pido perdón de antemano.

Descubrimiento

El primer paso es conseguir detectar un Mac desde un iPhone y viceversa. Mi primera idea era simplemente fuerza bruta, encontrar la máquina que estoy buscando literalmente revisando rangos de red.

Esto es una muy mala idea porque estamos hablando de 20+ minutos de tiempo de descubrimiento usando herramientas, si nos vamos a hacer probing de cada dirección IP es completamente ineficiente, especialmente en entornos como iOS en los que la batería es importante.

Bonjour

La respuesta a este problema es Bonjour, la implementación de Apple de un protocolo de configuración de redes llamado Zeroconf, basado en DNS (siempre son DNS) por las cuales un servidor se registra de manera local, y los clientes en cierta manera saben donde buscar. Esta tecnología lleva en funcionamiento desde 2002, y se convirtió en un estándar en 2013, así que puede ser el primer paso para nuestra conexión.

Seguridad

Una vez sabemos cómo vamos a encontrarnos, el siguiente paso es asegurar esa conexión. La primera sugerencia que vi fue usar mTLS con certificados en ambas partes, pero me parecía tal vez demasiado engorroso, así que buscando simplificaciones me encontré con PSK-TLS y SPAKE2, en la que se pre-comparte una clave entre ambos terminales y con ello, se cifra la conexión.

El problema de esto es que tengo que pasar una clave relativamente larga, así que recordé que tanto WhatsApp, varias implementaciones de Passkey y el sistema Cl@ve en España usan un código QR, con lo cual podría generar el QR desde el ordenador y que pudiera leer desde el móvil. Esta opción resultó ser incluso demasiado complicada así que tuve que simplificar el algoritmo, pero nos quedamos con el QR.

Comunicación

Una vez establecido el canal seguro, tenemos que pensar cómo comunicarnos. Yo llevo los últimos 15 años trabajando con JSON y texto plano, pero para este caso podíamos ir a un nivel más bajo. La primera idea era seguir esta línea y usar Protobuf, que es un formato de serialización que permite definir estructuras al estilo JSON.

Sin embargo, como con mTLS, esto era matar moscas a cañonazos así que manteniendo la simplicidad de bajo nivel, definir un mensaje con un prefijo que marcara el tamaño, podía ser suficiente. Al final, como simplificamos para hacer una conexión puntual, no hizo falta ni siquiera el prefijo, ya que se mandaba un único mensaje.

Interacción

El último punto interesante era la interacción con el portapapeles, necesitábamos poder leer y escribir tanto en iOS como en Mac, y reconozco que tenía un desconocimiento absoluto del sistema de eventos de Apple.

Como en otras implementaciones, el sistema de eventos de portapapeles de iOS no tiene nada que ver con el de Mac.

En el caso de iOS se usa UIPasteboard, y para el caso del portapapeles requiere mucho cariño: No existe un evento per se con lo cual tenemos que hacer polling al contenido del portapapeles cada X segundos, y cada vez que accedemos, tendremos una bonita advertencia de seguridad de Apple. Finalmente, como no es un evento, la aplicación ha de estar en primer plano para reaccionar. Pero bueno, hemos venido a jugar.

En el caso de macOS tenemos NSPasteboard, que es parecido, pero no igual, ya que tampoco hay un evento en concreto, sino que se utiliza el mismo proceso de polling que hemos visto en iOS, comprobando el contenido del portapapeles cada X segundos, pero con la facilidad de poder ejecutar nuestra app en segundo plano.

En la práctica: Uniendo todas las piezas

Con el plan en la mano, tocaba abrir Visual Studio Code, la extensión de Claude, y empezar a confirmar una vez más que «en teoría la práctica es la aplicación de la teoría, pero en la práctica no lo es».

Portapapeles

Empecemos por la aplicación de Mac. Para simplificar al máximo vamos a hacer una aplicación de consola, algo que podemos hacer con SwiftCLI y que además nos permite acceso al objeto NSPasteboard. Esto se resume en un único fichero Swift de unas 20 líneas que podemos compilar con swiftc.

El siguiente paso es la aplicación de iOS. Desafortunadamente no podemos hacer apps de consola para iOS, pero podemos hacer una aplicación minimalista que simplemente muestre por pantalla el contenido del portapapeles de UIPasteboard.

Incluso una aplicación completamente minimalista en iOS requiere un proyecto, un workspace, una App, una ContentView, y en nuestro caso un ObservableObject que responda a los cambios. La buena noticia es que podemos combinar todas esas clases en un único fichero Swift de unas 82 líneas (nada mal para una app de iOS con interfaz).

Conexión

Ya podemos capturar el portapapeles, vamos ahora a hacer que estos dos se vean. Para simplificar el ejercicio, vamos a usar la conexión solamente en una dirección, es decir, desde iOS hasta Mac.

Para agregar la conexión, como hemos visto anteriormente, necesitamos abrir la conexión usando Bonjour y un bucle infinito que espera nuevos mensajes, esto «engorda» nuestra app hasta las 60 líneas. Nada mal. En el lado de iOS necesitamos otro bucle que busque el servidor y que establezca la conexión, lo cual debería ser sencillo.

El modelo de Sandbox de iOS hace que para poder acceder a la red local tengamos que pedirlo por favor, y en el caso del simulador, no salta el permiso de uso de red local, así que no nos queda otra que usar un teléfono físico, al que tenemos que activar el modo desarrollador, también necesitamos estar registrados en Apple para tener un certificado personal para firmar las aplicaciones en el dispositivo, y finalmente en preferencias confiar en ese certificado personal. Vamos, todo sencillo. Tras batallar con todos estos pasos, finalmente teníamos conectividad!

La conexión es, sin lugar a dudas, lo que más dolores de cabeza me ha dado en el proyecto, ya que todo lo que hacía me rompía la conexión TCP, desde pasar la aplicación a segundo plano hasta el dichoso popup de «Permitir pegar».

Aquí necesito hacer una pausa, porque he de reconocer que estuve a punto de descartar el proyecto y el artículo porque no conseguía que la red funcionara, hasta que caí en la cuenta: no teníamos por qué mantener una conexión TCP abierta todo el tiempo.

En un teléfono, intentar mantener una conexión de red abierta constantemente es ridículo, el sistema operativo conspira para matarte, haces un gasto de batería innecesario y red innecesario, y necesitas gestionar todo el flujo de reconexión, especialmente para algo como la gestión de un portapapeles, donde podemos recibir el contenido, abrir la conexión, cifrarla y cerrarla.

Con ese descubrimiento, conseguimos tener la conexión bajo control, y pudimos pasar a la parte de cifrado.

Cifrado

El último paso era establecer una comunicación cifrada entre cliente y servidor. Como comentamos al principio, el plan incluía un QR. Para mi sorpresa puedo generar QRs desde la línea de comandos que se puedan escanear por un móvil.

En el caso de iOS, como siempre, el proceso fue más rebuscado, ya que para escanear el QR necesitamos permisos de cámara, y un botón para abrir la cámara.

Una vez solucionados los problemas de red, y teniendo en cuenta que la conexión no iba a ser persistente, esta última parte del proceso acabó siendo relativamente sencilla. Usé un cifrado bastante simple llamado ChaChaPoly, un cifrado simétrico bastante más moderno y rápido que el triple DES que vi en la carrera, y que se ejecuta por encima de la capa TCP, y así me simplificaba problemas relacionados con TLS.

Conclusiones y agentes

Este pequeño proyecto, que puedes encontrar en GitHub, ha sido poco más de una prueba de concepto para ayudarme a aprender cómo conectar dos sistemas parecidos y mandar información en texto plano.

Para la implementación me he apoyado mucho en Claude, concretamente en Opus 4.5 y 4.6, que me han permitido no solamente navegar la sintaxis de Swift, el framework de Cocoa, o los detalles de implementación de ChaChaPoly, sino que me ha permitido desplegar los cambios a mi teléfono sin tener que aprenderme los comandos de xcrun ni tener que estar buscando botones en Xcode

El proceso que seguí fue el siguiente: para la parte teórica, discutir con Claude (el chatbot) sobre las diferentes opciones (cómo acceder al portapapeles, cómo conectar iOS y Mac, y cómo securizar la conexión), y luego en la parte práctica, usando Claude Code, ir paso a paso implementando las fases, cometiendo errores, y dándome cuenta de mis errores de arquitectura.

No me cabe duda de que los modelos de lenguaje actuales me habrían permitido crear dos apps simplemente con un prompt en la línea de Create an app that captures the clipboard content in iOS, and sends it to another app running on mac., pero el objetivo de este ejercicio era establecer los diferentes bloques de la app, y que los implementara el agente, no delegar todo el razonamiento.

Como ejercicio, ha sido muy interesante y una manera más de demostrar que estas herramientas nos pueden ayudar siempre que sepamos lo que estamos haciendo ya que incluso los modelos de lenguaje más avanzados no nos van a salvar de una mala decisión técnica.

La magia al final del día es unir herramientas muy poderosas (Claude), tecnologías establecidas (Bonjour, ChaChaPoly, Pasteboard y Swift) un poco de imaginación y las ganas de aprender algo diferente.

Y tú, ¿has experimentado algo similar con los LLMs?

Actualización: Agregado enlace al repositorio. Gracias por el aviso @patoroco!


Posted

in

,

by

Comments

Deja un comentario

Este sitio utiliza Akismet para reducir el spam. Conoce cómo se procesan los datos de tus comentarios.