Korat
Escrito para quien lee con cuidado

Cuentas, accesos y lo que todavía no podemos hacer

Esta página está escrita como se le explicaría a otro ingeniero: qué protege tu cuenta, qué protege tus filas en la base de datos y — al final, en el mismo cuerpo de letra — la lista de cosas que Korat no hace y que no se le deben atribuir.

Dos ideas cargan con casi todo el peso. Tu cuenta es una sesión de autenticación de verdad, no una fila contra la que comparamos una contraseña. Y quien decide qué puedes leer es la base de datos, no la app — así que un cliente modificado, o alguien hablando directamente con la API con la clave pública, obtiene exactamente las mismas respuestas que la app.

Cuentas: bcrypt, una sesión real y un puente a tu perfil

La identidad vive en Supabase Auth. Las contraseñas se hashean con bcrypt en el servidor; la app nunca ve un hash y no hay ninguna comparación casera en el camino de inicio de sesión. Entrar produce un JWT con tu alcance, y una columna de tu fila de perfil (auth_uid) es el único vínculo entre el usuario de autenticación y el perfil — que es lo que hace expresable cada política de más abajo.

Puedes entrar con correo y contraseña, o con Google a través del Credential Manager de Android, que entrega un token de identidad de Google real a Supabase para el intercambio. Cerrar sesión en todas partes mata la sesión globalmente.

Queda una columna de PIN heredada de antes de Supabase Auth. Nadie la lee, las cuentas nuevas se crean con ella vacía y está en cola para borrarse. Se menciona aquí solo porque existe y la encontrarías.

Passkeys: verificados en el servidor, con guardia antirrepetición

Korat admite passkeys — un par de claves creado dentro del hardware seguro de tu teléfono, desbloqueado con tu huella o tu cara, que nunca sale del dispositivo. La parte de confianza es koratland.com, atada a la app Android mediante assetlinks.json, y la verificación corre en un Worker de Cloudflare y no en una biblioteca:

  • el reto es de un solo uso, con cinco minutos de vida, y la fila se borra al consumirse
  • el rpIdHash de los datos del autenticador debe ser igual al SHA-256 de koratland.com
  • la bandera de presencia de la persona tiene que estar puesta
  • la firma se verifica con WebCrypto — ECDSA P-256 o RS256
  • el contador de firma tiene que aumentar; un contador que se repite o retrocede se rechaza como repetición

El registro exige una sesión existente y toma la identidad de quien llama de su token de portador, nunca de un identificador puesto en el cuerpo de la petición — porque si no, cualquiera podría registrar un passkey en la cuenta de cualquiera. La fila de credencial guardada no tiene ninguna política de actualización, así que ni su dueño puede reescribir su propia clave pública ni su contador de firma; renombrar un passkey pasa por una función que solo puede tocar la etiqueta.

Una omisión deliberada: la atestación no se verifica. Korat no comprueba qué fabricante hizo el autenticador, así que nada de esto debe leerse como una afirmación atestiguada por hardware.

Estado honesto. El *registro* de passkeys está verificado y funciona. El *inicio de sesión* con passkey no se ha probado de principio a fin todavía, y la app web no admite passkeys en absoluto: es una función de Android en despliegue. Los caminos en uso diario son correo y contraseña, y el acceso con Google.

Seguridad a nivel de fila: responde la base de datos, no la app

La seguridad a nivel de fila está activada en toda la base de datos. Las políticas se escriben con cuatro funciones, y casi toda regla en Korat es una de ellas:

app_uid()
El id de perfil de quien llama, o nulo si no hay sesión. La base de toda política de «solo tus propias filas».
is_member_of(business_id)
Cierto para el propietario, o para quien tenga una fila de pertenencia activa en ese negocio.
has_capability(business_id, capability)
Expande los roles predefinidos — propietario, encargado, camarero, cocina — en el servidor, para que la base de datos nunca se fíe de lo que el cliente crea que significa un rol.
can_review(business_id)
Lee la vista de elegibilidad, que convierte «solo los clientes reales pueden reseñar» en una propiedad de la base de datos y no en una promesa de la app.

Tres reglas se aprendieron lo bastante caras como para dejarlas escritas. Una política escrita solo desde el punto de vista de la tienda excluye al cliente — una sesión de mesa, una llamada al personal y una transacción tienen dos lectores legítimos, así que se leen user_id = app_uid() or is_member_of(business_id). Una tabla hija debe ser legible exactamente hasta donde lo es su tabla madre, así que comentarios y reacciones delegan en la publicación en vez de llevar su propia idea de quién puede leerlos. Y una función de definidor corre como su propietario, así que tiene que comprobar el permiso ella misma.

Lo que la seguridad a nivel de fila no puede hacer, y qué lo sustituye

La seguridad a nivel de fila puede responder *quién llama*. Nunca puede responder *si esta petición traía un token válido* — un QR de mesa, un enlace compartido, una invitación. Así que todo camino con token en Korat es una función SECURITY DEFINER que comprueba el token ella misma, y la tabla se queda cerrada.

Entrar en una mesa. Quien está a punto de escanear un QR no es ni el anfitrión de la sesión ni personal, así que no puede leer la tabla de sesiones de mesa en absoluto, ni siquiera para buscar el token. join_session_by_token compara el token en el servidor, hace anfitrión a quien escanea primero, añade a la persona y no devuelve nada cuando el token es incorrecto, en vez de un mensaje con el que se pudiera acotar una conjetura.

Enlaces compartidos. resolve_share devuelve el tipo y el destino de un enlace y nada más: nunca quién lo compartió ni cuántos clics tiene. Contar un clic pasa por record_share_click, así que cualquiera puede ser contado sin ningún permiso de inserción en la tabla de analítica; deliberadamente no hay política de inserción para nadie.

El dinero. Hacer un pedido corre entero en el servidor. Pone el precio de cada línea él mismo y lee la tarifa de reparto de la propia fila del negocio, ignorando lo que mandó el cliente — porque una tarifa que el cliente puede fijar es un descuento que el cliente puede concederse.

Contactos, dispositivos y el inicio de sesión que no hiciste tú

El código de verificación de un correo de respaldo lo genera una función que solo puede llamar el rol de servicio. Solo se guarda el SHA-256 del código, caduca en diez minutos, admite cinco intentos y está limitado a una petición por minuto. El Worker que envía el correo nunca devuelve el código a la app.

No puedes marcar tu propio contacto como verificado. Un disparador de base de datos fuerza la marca a falso en cualquier escritura desde el cliente, y solo una función de definidor que haya comprobado de verdad un código puede ponerla. Parece paranoico hasta que lo sigues hasta el final: sin eso, añadir tu dirección a la cuenta de otra persona y pulsar «he olvidado la contraseña» es una toma de control completa. Por lo mismo, un contacto tiene que estar verificado antes de poder hacerse principal: la dirección principal es a la que escribe el sistema.

Todos los dispositivos con sesión aparecen en Ajustes y se pueden revocar de uno en uno. La tabla de dispositivos no tiene ninguna política de inserción, actualización ni borrado, porque un dispositivo recién revocado todavía tiene un token válido y si no podría borrar su propia revocación. Revocar un dispositivo borra la sesión subyacente, así que el token de refresco muere con ella.

Un inicio de sesión desde un dispositivo nuevo escribe el aviso dentro de la app en la misma transacción que crea la fila del dispositivo, para que no se pueda perder, y además manda un correo — solo a contactos verificados, porque una dirección sin verificar puede pertenecer a otra persona. El correo sale como mucho una vez por dispositivo y solo para dispositivos creados en la última hora, para que el endpoint no se pueda convertir en una forma de inundar a quien es dueño de la cuenta.

El límite de la revocación, dicho sin rodeos. Un token de acceso ya emitido vive alrededor de una hora y no se puede retirar a mitad de vuelo. Un dispositivo revocado que esté sin conexión sigue funcionando hasta que su token caduca. Esa es la naturaleza de los JWT, no un fallo, y por eso existe la lista de dispositivos en lugar de una afirmación de que la revocación es instantánea.

Lo que puedes hacer con tu propia cuenta

Borrar la cuenta se hace solo, desde Ajustes: no hay que escribir a nadie. Es una *solicitud* registrada y cancelable, no un botón que pulsas a las dos de la mañana y ya no puedes deshacer. Su estado se consulta dentro de la app. privacy@koratland.com sigue ahí para los demás derechos del PDPA: acceso, rectificación, portabilidad y retirada del consentimiento.

Denunciar cubre contenidos además de personas: perfiles, publicaciones, comentarios, historias, mensajes y páginas de negocio. La función que recibe una denuncia es SECURITY INVOKER, la única así en toda la trastienda. Como definer *vería* publicaciones ocultas y privadas y respondería distinto para ellas, lo que convierte el botón de denunciar en un oráculo sobre contenido que no te está permitido ver. Ejecutada como quien llama, «no existe esa publicación» y «esa publicación no es tuya para verla» se funden en una sola respuesta — lo cual es honesto, porque desde donde está quien denuncia son la misma cosa. El límite de frecuencia vive en un trigger, no en la app.

Lo que Korat no hace

Esta lista es la razón por la que merece la pena leer el resto de la página. Todo lo de aquí es cierto hoy.

  • Nada está cifrado de extremo a extremo. Los mensajes, los archivos y las llamadas están protegidos en tránsito por TLS y en reposo por seguridad a nivel de fila. El servidor puede leerlos. Korat no tiene claves en el dispositivo ni gestión de claves, y no debería elegirse para nada que las necesite.
  • No hay cifrado en reposo a nivel de aplicación más allá del que aporte la base de datos gestionada.
  • Nunca se ha comprobado un documento de identidad. No hay eKYC. Nada en Korat dice «identidad verificada», y las insignias de confianza dicen en su lugar qué pruebas existen.
  • Los números de teléfono no se pueden verificar: no hay proveedor de SMS conectado, y el endpoint lo dice en vez de fingir.
  • Los archivos subidos se pueden leer públicamente por URL. Avatares, imágenes de publicaciones e imágenes de chat viven en buckets públicos; escribir exige una sesión, pero una URL que se filtra es un archivo que se puede descargar. Todavía no hay URLs firmadas.
  • Las comprobaciones de permisos de la consola de negocio corren hoy en el dispositivo, con la misma matriz disponible en el servidor como has_capability(). Trata los permisos de la consola como un control organizativo, no como una frontera de seguridad, hasta que ese despliegue termine.
  • Las llamadas no tienen servidor TURN y no están cifradas de extremo a extremo; entre ciertas redes, algunas no llegarán a conectar.
  • No hay copias de seguridad de la base de datos en el plan actual, ni suite de pruebas automatizadas.
  • La app web no tiene notificaciones push. El push es una función de la app Android; en la web ves la notificación al abrir la pestaña, no cuando llega.

Si algo de eso cambia, esta página cambia con ello. Sería más fácil escribir un párrafo sobre cifrado de grado bancario y seguir adelante; esto es más útil. El razonamiento detrás de estas decisiones está en la página acerca de.

Preguntas frecuentes

¿Los mensajes de Korat están cifrados de extremo a extremo?

No. Los mensajes viajan por TLS y en la base de datos los protegen políticas de seguridad a nivel de fila que solo dejan leerlos a las dos personas de la conversación — pero el servidor puede leerlos. Si necesitas cifrado de extremo a extremo, Korat no es la herramienta adecuada para esa conversación.

¿Qué pasa si alguien me roba la contraseña?

Dispararía un aviso de dispositivo nuevo, dentro de la app y por correo a tus contactos verificados, la primera vez que entrase desde un dispositivo que no has usado. Puedes revocar ese dispositivo desde Ajustes, lo que borra su sesión, y cambiar la contraseña. Añadir un passkey saca la contraseña del ataque por completo en ese dispositivo.

¿Un negocio ve mi perfil porque le he hecho un pedido?

Un negocio ve lo que necesita para servir el pedido: tu nombre en la mesa, los artículos y, si es reparto, la dirección que escribiste. Cada campo de tu perfil lleva su propio ajuste de público, amigos o privado, aplicado cuando otra persona ve tu perfil. La fidelización y la lista de clientes se construyen desde el libro de ventas, no desde tu perfil.

¿Korat cumple la PDPA?

Korat pide consentimiento PDPA antes de crear una cuenta, en una pantalla que hay que desplazar hasta el final, con una vía real de rechazo. Dice qué se recoge y para qué, no vende datos personales y admite acceso, corrección, borrado, oposición, portabilidad y retirada del consentimiento a través de privacy@koratland.com. El cumplimiento es una obligación continua y no una insignia, y las lagunas listadas arriba forman parte de la respuesta honesta.

¿Cómo informo de un problema de seguridad?

Escribe a hello@koratland.com con los detalles. No hay programa de recompensas, pero un informe de verdad recibe una respuesta de verdad.

¿Preguntas que esta página no ha respondido?

Escribe a hello@koratland.com. Las preguntas de seguridad reciben una respuesta directa, incluido «eso todavía no lo hemos hecho».