Deux idées portent l’essentiel. Votre compte est une vraie session d’authentification, pas une ligne à laquelle nous comparons un mot de passe. Et c’est la base, pas l’app, qui décide de ce que vous avez le droit de lire — un client modifié, ou quelqu’un parlant directement à l’API avec la clé publique, obtient exactement les mêmes réponses que l’app.
Comptes : bcrypt, une vraie session, et un pont vers votre profil
L’identité vit dans Supabase Auth. Les mots de passe sont hachés en bcrypt sur le serveur ; l’app ne voit jamais d’empreinte de mot de passe et aucune comparaison maison n’existe dans le chemin de connexion. Se connecter produit un JWT à votre nom, et une colonne de votre ligne de profil (auth_uid) est le seul lien entre l’utilisateur d’authentification et le profil — c’est ce qui rend exprimable chacune des politiques décrites plus bas.
Vous pouvez vous connecter par e-mail et mot de passe, ou avec Google via le Credential Manager d’Android, qui remet un vrai jeton d’identité Google à Supabase pour échange. Se déconnecter partout tue la session globalement.
Il subsiste une colonne PIN héritée d’avant Supabase Auth. Rien ne la lit, les nouveaux comptes sont créés avec elle vide, et sa suppression est programmée. Elle n’est mentionnée ici que parce qu’elle existe et que vous la trouveriez.
Passkeys : vérifiées sur le serveur, avec une garde anti-rejeu
Korat prend en charge les passkeys — une paire de clés créée dans le matériel sécurisé de votre téléphone, déverrouillée par empreinte ou par visage, qui ne quitte jamais l’appareil. La partie de confiance est koratland.com, liée à l’app Android par assetlinks.json, et la vérification s’exécute dans un Worker Cloudflare plutôt que dans une bibliothèque :
- le défi est à usage unique, avec une durée de vie de cinq minutes, et la ligne est supprimée dès sa consommation
- le
rpIdHashdes données d’authentificateur doit être égal au SHA-256 dekoratland.com - l’indicateur de présence de l’utilisateur doit être positionné
- la signature est vérifiée par WebCrypto — ECDSA P-256 ou RS256
- le compteur de signature doit augmenter ; un compteur qui se répète ou recule est rejeté comme un rejeu
L’enregistrement exige une session existante et prend l’identité de l’appelant dans son jeton porteur, jamais dans un identifiant d’utilisateur passé dans le corps de la requête — sans quoi n’importe qui pourrait enregistrer une passkey sur le compte de n’importe qui. La ligne d’identifiant stockée n’a aucune politique de mise à jour, si bien que même son propriétaire ne peut réécrire sa clé publique ni son compteur de signature ; renommer une passkey passe par une fonction qui ne peut toucher que le libellé.
Une omission délibérée : l’attestation n’est pas vérifiée. Korat ne contrôle pas quel fabricant a produit l’authentificateur, donc rien ici ne doit être lu comme une garantie matérielle attestée.
État honnête. L’*enregistrement* d’une passkey est vérifié comme fonctionnel. La *connexion* par passkey n’a pas encore été exécutée de bout en bout, et l’app web n’a aucune prise en charge des passkeys — c’est une fonctionnalité Android en cours de déploiement. L’e-mail et mot de passe, ainsi que la connexion Google, sont les chemins utilisés au quotidien.
Sécurité au niveau des lignes : c’est la base qui répond, pas l’app
La sécurité au niveau des lignes est active dans toute la base. Les politiques s’écrivent avec quatre fonctions, et presque chaque règle de Korat est l’une d’elles :
- app_uid()
- L’identifiant de profil de l’appelant, ou null quand personne n’est connecté. La base de toute politique « seulement ma propre ligne ».
- is_member_of(business_id)
- Vrai pour le propriétaire, ou pour quiconque possède une ligne d’appartenance active à ce commerce.
- has_capability(business_id, capability)
- Développe les préréglages de rôles — propriétaire, responsable, salle, cuisine — côté serveur, si bien que la base ne croit jamais l’idée que le client se fait d’un rôle.
- can_review(business_id)
- Lit la vue d’éligibilité, qui fait de « seuls les vrais clients peuvent écrire un avis » une propriété de la base plutôt qu’une promesse de l’app.
Trois règles ont été apprises assez cher pour mériter d’être écrites. Une politique rédigée du seul point de vue du commerce exclut le client — une session de table, un appel au personnel et une transaction ont deux lecteurs légitimes, d’où user_id = app_uid() or is_member_of(business_id). Une table enfant doit être lisible exactement aussi loin que sa table parente, donc commentaires et réactions délèguent à la publication au lieu de porter leur propre idée de qui peut lire. Et une fonction definer s’exécute avec les droits du propriétaire, donc elle doit vérifier la permission elle-même.
Ce que la sécurité des lignes ne sait pas faire, et ce qui la remplace
La sécurité au niveau des lignes peut répondre à *qui appelle*. Elle ne peut jamais répondre à *cette requête portait-elle un jeton valide* — un QR de table, un lien de partage, une invitation. Chaque chemin à jeton dans Korat est donc une fonction SECURITY DEFINER qui vérifie le jeton elle-même, et la table reste fermée.
Rejoindre une table. Quelqu’un sur le point de scanner un QR n’est ni l’hôte de la session ni membre du personnel : il ne peut pas lire du tout la table des sessions de repas — pas même pour y chercher le jeton. join_session_by_token compare le jeton côté serveur, fait du premier scanneur l’hôte, ajoute le participant, et ne renvoie strictement rien quand le jeton est mauvais plutôt qu’un message qui servirait à affiner une supposition.
Liens de partage. resolve_share renvoie le type et la cible d’un lien, et rien d’autre — jamais qui a partagé, jamais le nombre de clics. Le comptage passe par record_share_click, si bien que n’importe qui peut être compté sans aucun droit d’insertion sur la table d’analyse ; il n’existe volontairement de politique d’insertion pour personne.
L’argent. Passer une commande s’exécute entièrement sur le serveur. Il calcule chaque ligne lui-même et lit les frais de livraison dans la ligne du commerce, en ignorant ce que le client a envoyé — parce que des frais que le client peut fixer sont une remise que le client peut s’accorder.
Contacts, appareils, et la connexion que vous n’avez pas faite
Un code de vérification pour une adresse de secours est généré par une fonction que seul le rôle de service peut appeler. Seul le SHA-256 du code est stocké, il expire en dix minutes, il autorise cinq tentatives, et il est limité à une demande par minute. Le Worker qui envoie le message ne renvoie jamais le code à l’app.
Vous ne pouvez pas marquer votre propre contact comme vérifié. Un déclencheur force l’indicateur à faux sur toute écriture client, et seule une fonction definer ayant réellement contrôlé un code peut le poser. Cela paraît paranoïaque jusqu’à ce qu’on suive le raisonnement : sans cela, ajouter votre adresse au compte de quelqu’un d’autre puis appuyer sur « mot de passe oublié » est une prise de contrôle complète. Pour la même raison, un contact doit être vérifié avant de pouvoir devenir principal — l’adresse principale est celle à laquelle le système écrit.
Chaque appareil connecté est listé dans les réglages et peut être révoqué individuellement. La table des appareils n’a aucune politique d’insertion, de mise à jour ni de suppression, parce qu’un appareil qui vient d’être révoqué détient encore un jeton valide et pourrait sinon effacer sa propre révocation. Révoquer un appareil supprime la session sous-jacente, donc le jeton de rafraîchissement meurt avec elle.
Une connexion depuis un nouvel appareil écrit l’alerte dans l’app dans la même transaction que la création de la ligne d’appareil, donc elle ne peut pas se perdre, et vous écrit aussi — uniquement à des contacts vérifiés, car une adresse non vérifiée peut appartenir à quelqu’un d’autre. Le message part au plus une fois par appareil et seulement pour un appareil créé dans la dernière heure, afin que l’endpoint ne puisse pas servir à inonder le titulaire du compte.
La limite de la révocation, dite franchement. Un jeton d’accès déjà émis vit environ une heure et ne peut pas être rappelé en vol. Un appareil révoqué mais déconnecté du réseau continue de fonctionner jusqu’à l’expiration de son jeton. C’est la nature des JWT, pas un bug, et c’est pourquoi il existe une liste d’appareils plutôt qu’une affirmation de révocation instantanée.
Ce que vous pouvez faire sur votre propre compte
La suppression du compte se fait tout seul, depuis les Réglages — aucun e-mail à écrire. C’est une *demande* enregistrée et annulable, pas un bouton qu’on presse à deux heures du matin sans retour possible. Son état est consultable dans l’app. privacy@koratland.com reste là pour les autres droits PDPA : accès, rectification, portabilité et retrait du consentement.
Le signalement couvre les contenus autant que les personnes — profils, publications, commentaires, stories, messages et pages de commerce. La fonction qui reçoit un signalement est SECURITY INVOKER, la seule dans tout le back-office. En definer, elle *verrait* les publications masquées et privées et répondrait différemment pour elles, ce qui transforme le bouton de signalement en oracle sur des contenus qu’on n’a pas le droit de voir. Exécutée avec les droits de l’appelant, « cette publication n’existe pas » et « cette publication ne vous est pas visible » se confondent en une seule réponse — ce qui est honnête, car depuis la place de la personne qui signale, ce sont la même chose. La limite de fréquence vit dans un trigger, pas dans l’app.
Ce que Korat ne fait pas
Cette liste est la raison pour laquelle le reste de la page vaut la lecture. Tout ce qui suit est vrai aujourd’hui.
- Rien n’est chiffré de bout en bout. Messages, fichiers et appels sont protégés en transit par TLS et au repos par la sécurité au niveau des lignes. Le serveur peut les lire. Korat n’a ni clés d’appareil ni gestion de clés, et ne doit pas être choisi pour ce qui en exige.
- Il n’y a pas de chiffrement applicatif au repos au-delà de ce que fournit la base de données managée.
- Aucune pièce d’identité n’a jamais été contrôlée. Il n’y a pas d’eKYC. Rien dans Korat ne dit « identité vérifiée », et les badges de confiance énoncent à la place la preuve qui existe.
- Les numéros de téléphone ne peuvent pas être vérifiés — aucun fournisseur SMS n’est raccordé, et l’endpoint le dit au lieu de faire semblant.
- Les médias déposés sont lisibles publiquement par URL. Avatars, images de publications et images de conversations vivent dans des buckets publics ; l’envoi exige une session, mais une URL qui fuit est un fichier que l’on peut récupérer. Il n’y a pas encore d’URL signées.
- Les contrôles de permissions de la console professionnelle s’exécutent actuellement sur l’appareil, la même matrice étant disponible côté serveur sous
has_capability(). Traitez les permissions de la console comme un contrôle organisationnel, pas comme une frontière de sécurité, tant que ce déploiement n’est pas terminé. - Les appels n’ont pas de serveur TURN et ne sont pas chiffrés de bout en bout ; certaines connexions entre réseaux différents échoueront.
- Il n’y a pas de sauvegarde de base de données sur l’offre actuelle, ni de suite de tests automatisés.
- L’app web n’a pas de notifications push. Le push est une fonction de l’app Android ; sur le web, vous voyez une notification en ouvrant l’onglet, pas au moment où elle arrive.
Si l’une de ces lignes change, cette page change avec elle. Il serait plus facile d’écrire un paragraphe sur un chiffrement de niveau bancaire et de passer à autre chose ; ceci est plus utile. Le raisonnement derrière ces choix est sur la page à propos.