Skip to content

Gestion des identités

Après avoir importé et instancié le SDK, il faudra qu'il dispose d'une "identité" pour effectuer d'autres opérations.

Un utilisateur est représenté par plusieurs "identités", la plupart du temps une par appareil. Ces identités sont créées au travers du SDK. Elles peuvent être simplement gardées en mémoire volatile, ou au contraire dans une base de données locale persistante. Elles peuvent également être sauvegardées sur nos serveurs de façon sécurisée pour pouvoir les réutiliser par la suite.

Pour sauvegarder des identités sur nos serveurs, il existe deux modes de protection :

Pour une gestion manuelle de la protection d'une identité que ne couvriraient pas ces plugins, n'hésitez pas à nous contacter ou utiliser l'export manuel.

uml diagram

Créer des identités

Un utilisateur de Seald est représenté par plusieurs "identités". Ce sont concrètement des clés privées générées localement sur l'appareil de l'utilisateur avec le SDK.

Elles sont toutes reliées entre elles par une chaîne de signature. Pour plus de détails sur notre technologie, consultez la page dédiée.

On distingue deux cas :

  • une première identité ;
  • les identités suivantes qui y sont rattachées que l'on appelle "sous-identités".

Première identité

Pour créer une identité pour un nouvel utilisateur, il faut utiliser la fonction sdk.initiateIdentity.

Cette fonction nécessite un JSON Web Token. Un guide dédié vous expliquera comment générer ces JWT de licence.

js
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKSPassword(keyStorageURL)] })
await seald.initialize()
// Créer son identité Seald
await seald.initiateIdentity({ signupJWT: 'signup-jwt-value' })
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKSPassword(keyStorageURL)] })
await seald.initialize()
// Créer son identité Seald
await seald.initiateIdentity({ signupJWT: 'signup-jwt-value' })

Identifiant personalisé

Il est possible d'ajouter un identifiant personnalisé, appelé connector, au format string, via l'utilisation d'un JWT. Ceci peut être fait lors de la création d'une identité, ou plus tard grâce à la fonction sdk.pushJwt.

Le connector doit être au format ${IDENTIFIANT}@${APP_ID}. Le type du connector doit toujours être 'AP'.

Une identité peut avoir plusieurs identifiants. L'ajout d'un identifiant personnalisé se fait via la clé connector_add définie dans cette documentation.

TIP

L'ancien nom, aujourd'hui déprécié, de cette fonctionnalité, était userId. Le userId correspondait à la partie IDENTIFIANT du format de connector décrit ci-dessus, sans le @${APP_ID}.

Sous-identités

À partir d'une identité existante d'un utilisateur on peut créer une "sous-identité" à l'aide de la fonction sdk.createSubIdentity avec une instance du SDK déjà initialisée.

Cette sous-identité pourra alors déchiffrer tout ce que peut déchiffrer l'identité initiale : lorsqu'un autre utilisateur chiffrera quelque chose pour cet utilisateur, l'identité et la sous-identité pourront toutes deux le déchiffrer.

On l'utilise typiquement pour avoir une sous-identité distincte par appareil. Par exemple, lors de la connexion de l'utilisateur à un nouvel appareil, on peut récupérer une identité "principale" protégée par mot de passe ou en 2-man rule, puis créer une nouvelle sous-identité liée à celle-ci pour cet appareil. Il est alors possible de stocker cette sous-identité en localStorage, et de la récupérer lors de l'ouverture d'une nouvelle page, afin d'avoir des sessions persistantes.

js
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKSPassword(keyStorageURL)] })
await seald.initialize()
// Créer son identité Seald
await seald.initiateIdentity({ signupJWT: 'signup-jwt-value' })

// Créer une sous-identité
const { subIdentity } = await seald.createSubIdentity()
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKSPassword(keyStorageURL)] })
await seald.initialize()
// Créer son identité Seald
await seald.initiateIdentity({ signupJWT: 'signup-jwt-value' })

// Créer une sous-identité
const { subIdentity } = await seald.createSubIdentity()

Cette fonction rend un Buffer similaire à celui créé par sdk.exportIdentity et décrit dans le paragraphe Export manuel.

TIP

Pour accélérer la création de sous-identités, vous pouvez pré-générer les clés privées en appelant la fonction seald.preGenerateIdentityKeys() à l'avance.

Exemples
js
// Pré-générer 1 clé
sdk.preGenerateIdentityKeys(1)

/*
Faire d'autres choses, pour laisser le temps à la pré-génération de clés de
se faire en tâche de fond
*/

// Créer une sous-identité de façon accélérée
const { identity } = await seald.createSubIdentity()
// Pré-générer 1 clé
sdk.preGenerateIdentityKeys(1)

/*
Faire d'autres choses, pour laisser le temps à la pré-génération de clés de
se faire en tâche de fond
*/

// Créer une sous-identité de façon accélérée
const { identity } = await seald.createSubIdentity()

Vous pouvez alors importer ce Buffer dans une autre instance du SDK avec la fonction sdk.importIdentity, ou directement le sauvegarder sur SSKS avec les fonctions sdk.ssks2MR.saveIdentity ou sdk.ssksPassword.saveIdentity.

WARNING

Avoir un très grand nombre de sous-identités pour le même utilisateur (plusieurs dizaines) peut ralentir les opérations concernant cet utilisateur, que ce soit la création de nouvelle sous-identité ou même simplement la création d'une session de chiffrement dont il est destinataire.

Dans le cas où un utilisateur aurait un très grand nombre d'appareils, ou se reconnecterait très régulièrement, nous vous conseillons de ne pas créer une sous-identité distincte à chaque connexion, mais simplement utiliser directement l'identité stockée sur SSKS.

Base de données locale persistante

Principe de fonctionnement

Par défaut, le SDK Seald n'enregistre sa base de données qu'en mémoire volatile : si vous créez une identité, puis fermez et réouvrez votre onglet de navigateur, l'identité créée ne sera plus accessible et sera perdue, sauf si vous l'avez protégée sur SSKS avec un mot de passe ou en two-man-rule. Dans ce cas, bien qu'ayant une session authentifiée, l'utilisateur doit retaper son mot de passe, ou s'authentifier par email, pour récupérer son identité Seald.

Pour améliorer l'expérience utilisateur, on peut utiliser une base de données locale persistante, automatiquement stockée par le SDK Seald dans le navigateur de façon sécurisée, chiffrée par une clée stockée en backend appelée databaseKey.

TIP

La base de donnée de Seald utilise nedb, qui dans le navigateur, n'utilise pas forcément le localStorage, mais choisit (via localForage) la meilleure méthode de stockage entre IndexedDB, WebSQL ou localStorage selon votre navigateur.

uml diagram

Lorsque l'utilisateur va rouvrir l'application, la databaseKey sera récupérée depuis le backend via une session authentifiée, et cette clé sera utilisée pour déchiffrer l'encryptedDB.

uml diagram

Utilisation d'une base de données persistante

Pour utiliser une base de données persistante, il suffit d'ajouter un argument databasePath pour enregistrer la base de données, et un argument databaseKey pour la chiffrer, à l'initialisation du SDK.

Le serveur peut soit utiliser une unique databaseKey par utilisateur pour tous ses appareils, soit au contraire utiliser une databaseKey différente par session. Utiliser une databaseKey différente par session est un peu plus sécurisé, mais implique un modèle de donnée un peu plus compliqué. Nous supposerons dans la suite une databaseKey différente par session.

js
const seald = SealdSDK({
  appId,
  apiURL,
  databaseKey,
  databasePath: `seald-guide-session-${sessionID}`
})
const seald = SealdSDK({
  appId,
  apiURL,
  databaseKey,
  databasePath: `seald-guide-session-${sessionID}`
})

Ici, databaseKey et sessionID doivent être générés et rendus par le serveur.

On rajoute sessionID dans le databasePath afin que celui-ci soit unique pour chaque session.

En effet, si un même utilisateur se déconnecte, puis se reconnecte, le serveur aura généré une nouvelle databaseKey. On ne peut donc pas utiliser la même base de données : il faut que la base de données soit unique pour chaque session, on fait donc varier le databasePath en fonction du sessionID.

TIP

Vous pouvez aussi remplacer databaseKey par databaseRawKey.

Attention, databaseRawKey doit absolument être un string représentant l'encodage Base64 d'un buffer cryptographiquement aléatoire de 64 octets.

Techniquement, ceci permet d'éviter que databaseKey soit dérivé avec scrypt, ce qui améliore la vitesse de l'instanciation.

Si vous avez le moindre doute, utilisez plutôt databaseKey.

Exemples

Utilisation de databaseRawKey :

js
// front-end
const seald = SealdSDK({
  appId,
  apiURL,
  databaseRawKey: 'uz0BqYCF6IzefVHd8VzvbOZVp12GTMFD2L+UwCGYiRbKhGymgwG5HMSGfNiDt37h5FaTMKHYaqCcGTtH2ZVCzw=='
  databasePath: `seald-guide-session-${sessionID}`
})
// front-end
const seald = SealdSDK({
  appId,
  apiURL,
  databaseRawKey: 'uz0BqYCF6IzefVHd8VzvbOZVp12GTMFD2L+UwCGYiRbKhGymgwG5HMSGfNiDt37h5FaTMKHYaqCcGTtH2ZVCzw=='
  databasePath: `seald-guide-session-${sessionID}`
})

Génération d'une databaseRawKey adéquate :

js
// back-end
const crypto = require('crypto')
const util = require('util')
const randomBytes = util.promisify(crypto.randomBytes)

const rawKeyBuffer = await randomBytes(64)
// Ce format est strict : cela doit être exactement 64 octets, encodés en Base64
// et provenir d'une source d'aléa cryptographiquement sûre
const rawKey = rawKeyBuffer.toString('base64')
// back-end
const crypto = require('crypto')
const util = require('util')
const randomBytes = util.promisify(crypto.randomBytes)

const rawKeyBuffer = await randomBytes(64)
// Ce format est strict : cela doit être exactement 64 octets, encodés en Base64
// et provenir d'une source d'aléa cryptographiquement sûre
const rawKey = rawKeyBuffer.toString('base64')

WARNING

Si databasePath est défini, mais que databaseKey (ou databaseRawKey) ne l'est pas, le SDK utilisera une clé de chiffrement fixe pour la base de données (dérivée du appId), ce qui est une mauvaise pratique et peut représenter un risque mineur de sécurité.

Contrairement aux modes de protection de l'identité sur SSKS, ici le fonctionnement est identique que vous vouliez créer une nouvelle identité et la stocker dans une base de donnée persistante, ou au contraire en récupérer une existante.

Vous trouverez plus de détails, notamment sur ce que vous devez implémenter coté backend, sur la page du projet exemple à propos de la base de données persistante.

Protection par mot de passe

La protection par mot de passe de l'identité se fonde sur une dérivation d'un mot de passe connu de l'utilisateur en une clé symétrique que l'on utilise pour chiffrer et déchiffrer l'identité.

WARNING

Une fonctionnalité de type "mot de passe oublié" n'est pas implémentable avec cette méthode sans perte de donnée. Si elle est nécessaire dans votre application, utilisez plutôt la protection en 2-man-rule.

Principe de fonctionnement

Cette identité chiffrée est ensuite stockée sur un service dédié nommé Seald SDK Key Storage ou SSKS.

La protection par mot de passe fonctionne de la façon suivante :

uml diagram

Et la récupération de la façon suivante :

uml diagram

Le plugin @seald-io/sdk-plugin-ssks-password permet d'effectuer ces opérations automatiquement.

TIP

Plus précisément, le mot de passe de l'utilisateur est dérivé deux fois :

  • une première fois en le combinant avec le userId et le appId pour donner une "clé de stockage" qui n'est pas secrète ;
  • une deuxième fois en le combinant avec un "sel" aléatoire, le userId et le appId pour donner une "clé de chiffrement" qui elle est secrète.

L'identité est chiffrée avec la "clé de chiffrement". Puis, sont envoyés à SSKS :

  • la "clé de stockage" ;
  • le "sel" concaténé avec l'identité chiffrée.

L'identité est récupérée en envoyant à SSKS la clé de stockage qui répond le sel concaténé avec l'identité chiffrée. Le sel est alors utilisé pour obtenir la clé de chiffrement qui est à son tour utilisée pour déchiffrer l'identité.

Sauvegarder l'identité

Pour utiliser cette methode de stockage, il faut ajouter le plugin @seald-io/sdk-plugin-ssks-password lors de l'instanciation du SDK, puis, après la création de l'identité, la stocker sur SSKS à l'aide de la fonction sdk.ssksPassword.saveIdentity.

javascript
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKSPassword(keyStorageURL)] })
await seald.initialize()
// Créer son identité Seald
await seald.initiateIdentity({ signupJWT: 'signup-jwt-value' })
// Sauvegarder l'identité Seald, chiffrée par le mot de passe, sur le serveur SSKS
await seald.ssksPassword.saveIdentity({
  userId: 'myUserId',
  password: 'user-known-secret-password'
})
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKSPassword(keyStorageURL)] })
await seald.initialize()
// Créer son identité Seald
await seald.initiateIdentity({ signupJWT: 'signup-jwt-value' })
// Sauvegarder l'identité Seald, chiffrée par le mot de passe, sur le serveur SSKS
await seald.ssksPassword.saveIdentity({
  userId: 'myUserId',
  password: 'user-known-secret-password'
})

L'identité est désormais sauvegardée.

TIP

Vous pouvez aussi sauvegarder une autre sous-identité, plutôt que l'identité de l'instance actuelle du SDK :

javascript
// Créer une sous-identité
const { identity: mySubIdentity } = await seald.createSubIdentity()
// Sauvegarder la nouvelle identité Seald, chiffrée par le mot de passe, sur le serveur SSKS
await seald.ssksPassword.saveIdentity({
  userId: 'myUserId',
  password: 'user-known-secret-password',
  identity: mySubIdentity
})
// Créer une sous-identité
const { identity: mySubIdentity } = await seald.createSubIdentity()
// Sauvegarder la nouvelle identité Seald, chiffrée par le mot de passe, sur le serveur SSKS
await seald.ssksPassword.saveIdentity({
  userId: 'myUserId',
  password: 'user-known-secret-password',
  identity: mySubIdentity
})

Récupérer l'identité

Pour récupérer l'identité à la connexion, il faut appeler la fonction sdk.ssksPassword.retrieveIdentity, avec les mêmes arguments userId et password que ceux utilisés lors de la sauvegarde de l'identité

TIP

Si vous utilisez plusieurs fois un mot de passe incorrect, le serveur peut limiter vos requêtes. Dans ce cas, vous recevrez le message d'erreur suivant : Request throttled, retry after {N}s (ce qui signifie Requête bloquée, réessayer après {N}s), où {N} est le nombre de secondes pendant lesquelles vous ne pouvez pas réessayer.

javascript
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKSPassword(keyStorageURL)] })
await seald.initialize()
// Récupérer l'identité Seald, chiffrée par le mot de passe, depuis le serveur SSKS
await seald.ssksPassword.retrieveIdentity({
  userId: 'myUserId',
  password: 'user-known-secret-password'
})
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKSPassword(keyStorageURL)] })
await seald.initialize()
// Récupérer l'identité Seald, chiffrée par le mot de passe, depuis le serveur SSKS
await seald.ssksPassword.retrieveIdentity({
  userId: 'myUserId',
  password: 'user-known-secret-password'
})

L'instance du SDK est désormais prête à être utilisée !

WARNING

Il est déconseillé de récupérer la même identité avec ssksPassword.retrieveIdentity sur plusieurs appareils en même temps, au même instant exact, par exemple lors de tests automatisés. Veuillez attendre que l'un des appareils ait fini de récupérer l'identité avant de lancer la récupération sur un autre appareil.

Changement de mot de passe

Lorsque l'utilisateur change de mot de passe, vous pouvez utiliser la fonction sdk.ssksPassword.changeIdentityPassword pour que l'identité soit stockée avec le nouveau mot de passe et supprimer celle stockée avec l'ancien.

javascript
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKSPassword(keyStorageURL)] })
await seald.initialize()
// Récupérer l'identité Seald, chiffrée par l'ancien mot de passe, depuis le serveur SSKS
await seald.ssksPassword.retrieveIdentity({
  userId: 'myUserId',
  password: 'user-known-secret-password' // ancien mot de passe
})
// Changer le mot de passe
await seald.ssksPassword.changeIdentityPassword({
  userId: 'myUserId',
  currentPassword: 'user-known-secret-password', // ancien mot de passe
  newPassword: 'new-user-known-secret-password' // nouveau mot de passe
})
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKSPassword(keyStorageURL)] })
await seald.initialize()
// Récupérer l'identité Seald, chiffrée par l'ancien mot de passe, depuis le serveur SSKS
await seald.ssksPassword.retrieveIdentity({
  userId: 'myUserId',
  password: 'user-known-secret-password' // ancien mot de passe
})
// Changer le mot de passe
await seald.ssksPassword.changeIdentityPassword({
  userId: 'myUserId',
  currentPassword: 'user-known-secret-password', // ancien mot de passe
  newPassword: 'new-user-known-secret-password' // nouveau mot de passe
})

Le mot de passe est désormais changé et seul le nouveau peut être utilisé.

TIP

Si l'ancien mot de passe n'est pas connu (mot de passe oublié typiquement), ce plugin ne permet pas de récupérer l'identité. Si la fonctionnalité de "mot de passe oublié" est nécessaire dans votre application, utilisez plutôt la protection en 2-man-rule.

WARNING

Cette opération ne change pas les clés de l'identité elle-même, elle ne fait que la re-protéger avec un nouveau mot de passe. Si l'ancien mot de passe est considéré comme compromis il faut aussi créer une nouvelle identité et révoquer l'ancienne.

Mot de passe utilisé aussi pour l'authentification

Dans le cas où vous voudriez utiliser le même mot de passe pour protéger l'identité que pour l'authentification, celui-ci ne peut être transmis au serveur tel quel, sinon le serveur pourrait alors reconstituer la clé de l'utilisateur, brisant ainsi le chiffrement de bout-en-bout.

Il faut alors modifier les étapes de connexion et de création de compte pour introduire à une pré-dérivation du mot de passe à l'aide de fonctions de dérivation sécurisées de sorte que votre serveur n'ait jamais accès au mot de passe brut, mais seulement à une version dérivée.

Vous trouverez un exemple d'implémentation ici.

TIP

Dans le cas où vous ne pourriez pas modifier la fonction d'authentification (par exemple en cas d'authentification fédérée), il faudrait alors utiliser un second mot de passe ou un autre mode de protection de l'identité comme le 2-man rule.

Personnaliser la dérivation de mot de passe

Comme expliqué précédemment, le mot de passe fourni par l'utilisateur est dérivé en une "clé de stockage", et une "clé de chiffrement". Cette dérivation s'effectue avec scrypt.

Dans le cadre d'une utilisation avancée, vous pouvez néanmoins court-circuiter ce mécanisme, et directement passer une "clé de stockage" et une "clé de chiffrement" brutes en arguments.

La clé de stockage n'a pas besoin d'être secrète par rapport à votre serveur. Il faut cependant absolument qu'elle soit unique, et secrète de tout autre utilisateur, car elle permettrait de supprimer l'identité enregistrée. Elle doit être un string, de 256 caractères au maximum, et composé des caractères autorisés suivants: /A-Za-z0-9+\/=\-_@./.

Pour celle-ci, plusieurs stratégies sont possibles :

  • vous pouvez utiliser un string aléatoire générée par le serveur, et la passer simplement au front-end
  • vous pouvez également le dériver du mot de passe, potentiellement avec un autre sel stocké sur le serveur

La clé de chiffrement ne peut être connue que de l'utilisateur dans son frontend et doit absolument rester secrète de tout autre acteur, y compris votre serveur. Le format attendu est un string représentant 64 octets encodés en Base64. La manière usuelle de faire est de la dériver du mot de passe, si possible avec un sel unique aléatoire généré et stocké par votre serveur.

Exemple de dérivation personnalisée de rawEncryptionKey avec PBKDF2 :

javascript
const PBKDF2 = require('pbkdf2')
const util = require('util')
const pbkdf2 = util.promisify(PBKDF2.pbkdf2)

const derivedKeyBuffer = await pbkdf2(userPassword, serverStoredSalt, 1, 64, 'sha512')
const derivedKey = derivedKeyBuffer.toString('base64')

await sdk.ssksPassword.saveIdentity({
  userId,
  // Le format de `rawEncryptionKey` est strict :
  // cela doit être exactement 64 octets, encodés en Base64
  rawEncryptionKey: derivedKey,
  
  // `rawStorageKey` est plus souple : elle doit être unique et secrète,
  // mais peut être un string arbitraire, jusqu'à 256 caractères de long
  // Caractères autorisés : /A-Za-z0-9+\/=\-_@./
  rawStorageKey: serverStoredRawStorageKey
})
const PBKDF2 = require('pbkdf2')
const util = require('util')
const pbkdf2 = util.promisify(PBKDF2.pbkdf2)

const derivedKeyBuffer = await pbkdf2(userPassword, serverStoredSalt, 1, 64, 'sha512')
const derivedKey = derivedKeyBuffer.toString('base64')

await sdk.ssksPassword.saveIdentity({
  userId,
  // Le format de `rawEncryptionKey` est strict :
  // cela doit être exactement 64 octets, encodés en Base64
  rawEncryptionKey: derivedKey,
  
  // `rawStorageKey` est plus souple : elle doit être unique et secrète,
  // mais peut être un string arbitraire, jusqu'à 256 caractères de long
  // Caractères autorisés : /A-Za-z0-9+\/=\-_@./
  rawStorageKey: serverStoredRawStorageKey
})

Protection en 2-man-rule

Le mode de protection en 2-man-rule consiste à "couper" en deux l'identité à protéger et de placer une moitié sur le back-end de l'application soumis à authentification, et l'autre moitié sur Seald SDK Key Storage (SSKS) soumis à une authentification indépendante.

L'objectif est qu'un utilisateur ait "le droit" de perdre tous ses mots de passe et tous ses appareils sans perte de donnée. La contrepartie est que ce mode n'est pas strictement "de bout-en-bout".

Il est nécessaire d'implémenter côté serveur le stockage d'un secret stocké et remis contre authentification, ainsi qu'une interaction avec SSKS.

Principe de fonctionnement

Plus concrètement, pour protéger une identité, le protocole est le suivant (dans le cas classique) :

uml diagram

TIP

Dans le cas où une identité a déjà été stockée sur SSKS avec cette adresse email, le serveur SSKS demandera alors une authentification par challenge email, similaire à celle décrite dans la section suivante à propos de la récupération d'une identité, avant de permettre le stockage d'une nouvelle identité.

Pour plus de détails, référez-vous à la page Intégrer le 2-man-rule sur votre backend.

Pour récupérer une identité, on procède de la façon suivante :

uml diagram

Ainsi si le backend est compromis, la encryptedIdentity stockée sur SSKS ne le sera pas, et le backend ne peut pas simuler une authentification auprès de SSKS.

Inversement si SSKS est compromis, la clé twoManRuleKey ne le sera pas et SSKS ne peut pas simuler une authentification au backend.

WARNING

Ce mode n'est pas strictement de bout-en-bout dans le sens où la clé permettant le déchiffrement des données d'un utilisateur peut être récupérée sans le consentement de l'utilisateur en réunissant twoManRuleKey sur SSKS et encryptedIdentity sur le backend.

TIP

Seald prend les précautions suivantes pour assurer la confidentialité des données stockées sur SSKS :

  • utilisation de serveurs non soumis au Cloud Act (chez OVH en France) pour que le mode de protection par 2-man rule soit robuste dans le cadre d'une utilisation d'hébergeurs soumis à l'extra-territorialité du droit américain pour le backend ;
  • sur-chiffrement au repos des champs sensibles (encryptedIdentity) ;
  • hachage et salage des adresses e-mail des utilisateurs auxquelles sont associées les encryptedIdentity, de sorte qu'elles ne soient connues que lors de l'exécution. Ainsi, une compromission à froid de SSKS ne permet pas d'associer les encryptedIdentity aux adresses e-mail.

WARNING

Les adresses emails et numéros de téléphones envoyés au serveur SSKS doivent absolument être normalisés.

Protection d'une identité

Pour utiliser cette methode de protection, il faut installer, importer & ajouter lors de l'instanciation du SDK le plugin @seald-io/sdk-plugin-ssks-2mr.

Votre serveur applicatif doit ensuite :

  • créer un nouvel utilisateur sur SSKS en donnant son adresse email ;
  • générer une session SSKS pour celui-ci ;
  • générer une clé secrète twoManRuleKey pour cet utilisateur (une chaine de caractère aléatoire suffisamment robuste, par exemple un UUID serait un bon choix)
  • transmettre le sessionId et la clé secrète twoManRuleKey au front-end que nous symboliserons par API.getSSKSSession() dans l'exemple.

L'utilisateur peut alors stocker son identité à l'aide de la fonction sdk.ssks2MR.saveIdentity.

Vous trouverez la documentation de l'API de SSKS pour votre serveur applicatif ici.

javascript
// On instancie le SDK
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKS2MR(keyStorageURL)] })
await seald.initialize()
// On crée une identité Seald comme vu dans la première partie
await seald.initiateIdentity({ signupJWT: 'signup-jwt-value' })

// On fait un appel API au serveur de l'application pour récupérer le `sessionId` et le `twoManRuleKey`
const { sessionId, twoManRuleKey } = APIClient.getSSKSSession() // point d'API à développer de votre côté

await seald.ssks2MR.saveIdentity({
  userId: 'myUserId',
  sessionId,
  // l'adresse e-mail de l'utilisateur doit être répétée pour éviter un MITM par votre sevreur applicatif
  authFactor: {
    type: 'EM',
    value: 'user@domain.com'
  },
  twoManRuleKey // `twoManRuleKey` est la clé stockée par votre serveur applicatif pour protéger l'identité de cet utilisateur
})
// On instancie le SDK
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKS2MR(keyStorageURL)] })
await seald.initialize()
// On crée une identité Seald comme vu dans la première partie
await seald.initiateIdentity({ signupJWT: 'signup-jwt-value' })

// On fait un appel API au serveur de l'application pour récupérer le `sessionId` et le `twoManRuleKey`
const { sessionId, twoManRuleKey } = APIClient.getSSKSSession() // point d'API à développer de votre côté

await seald.ssks2MR.saveIdentity({
  userId: 'myUserId',
  sessionId,
  // l'adresse e-mail de l'utilisateur doit être répétée pour éviter un MITM par votre sevreur applicatif
  authFactor: {
    type: 'EM',
    value: 'user@domain.com'
  },
  twoManRuleKey // `twoManRuleKey` est la clé stockée par votre serveur applicatif pour protéger l'identité de cet utilisateur
})

L'identité est sauvegardée.

TIP

Vous pouvez aussi sauvegarder une autre sous-identité, plutôt que l'identité de l'instance actuelle du SDK :

javascript
// Créer une sous-identité
const { identity: mySubIdentity } = await seald.createSubIdentity()
// Sauvegarder la nouvelle identité Seald, chiffrée par `twoManRuleKey`, sur le serveur SSKS
await seald.ssks2MR.saveIdentity({
  userId: 'myUserId',
  sessionId,
  authFactor: {
    type: 'EM',
    value: 'user@domain.com'
  },
  twoManRuleKey,
  identity: mySubIdentity
})
// Créer une sous-identité
const { identity: mySubIdentity } = await seald.createSubIdentity()
// Sauvegarder la nouvelle identité Seald, chiffrée par `twoManRuleKey`, sur le serveur SSKS
await seald.ssks2MR.saveIdentity({
  userId: 'myUserId',
  sessionId,
  authFactor: {
    type: 'EM',
    value: 'user@domain.com'
  },
  twoManRuleKey,
  identity: mySubIdentity
})

TIP

Le serveur SSKS ne peut alors pas accéder à l'identité de l'utilisateur, car il n'a pas accès à la clé secrète twoManRuleKey gardée par votre serveur. Réciproquement, votre serveur applicatif ne peut pas y accéder non plus car pour récupérer une identité il faut un challenge envoyé par email ou SMS, qu'il ne reçoit jamais et ne peut pas forger.

Récupération d'une identité

Ensuite, lors d'une nouvelle instanciation du SDK, votre backend doit de la même manière générer une session pour l'utilisateur, et transmettre le sessionId et la clé twoManRuleKey au front-end. Le serveur SSKS envoie alors un email contenant un challenge (valable 6h) à l'adresse email de l'utilisateur.

TIP

Dans un environnement de test, votre serveur peut utiliser l'argument fake_otp: true lors de la génération de session, afin de ne pas réellement envoyer le challenge. Celui-ci sera alors fixé à 'aaaaaaaa'.

Pour plus de détail, voir le paragraphe dédié dans le guide à propos du 2-man-rule.

Il faut ensuite appeler la fonction sdk.ssks2MR.retrieveIdentity avec les arguments userId, sessionId, email, challenge et twoManRuleKey.

javascript
// On instancie le SDK
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKS2MR(keyStorageURL)] })
await seald.initialize()

// On fait un appel API au serveur de l'application pour récupérer le `sessionId` et la `twoManRuleKey`
const { sessionId, twoManRuleKey } = APIClient.getSSKSSession() // point d'API à développer

/* un email est envoyé à l'utilisateur contenant un `challenge` valable 6h */
/* ... */
/* le `challenge` est récupéré par l'utilisateur et donné dans le contexte de la page */

await seald.ssks2MR.retrieveIdentity({
  userId: 'myUserId',
  sessionId,
  // l'adresse e-mail de l'utilisateur doit être répétée pour éviter un MITM par votre sevreur applicatif
  authFactor: {
    type: 'EM',
    value: 'user@domain.com'
  },
  challenge: 'challenge envoyé par email',
  twoManRuleKey // `twoManRuleKey` est la clé stockée par votre serveur applicatif pour protéger l'identité de cet utilisateur
})
// On instancie le SDK
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKS2MR(keyStorageURL)] })
await seald.initialize()

// On fait un appel API au serveur de l'application pour récupérer le `sessionId` et la `twoManRuleKey`
const { sessionId, twoManRuleKey } = APIClient.getSSKSSession() // point d'API à développer

/* un email est envoyé à l'utilisateur contenant un `challenge` valable 6h */
/* ... */
/* le `challenge` est récupéré par l'utilisateur et donné dans le contexte de la page */

await seald.ssks2MR.retrieveIdentity({
  userId: 'myUserId',
  sessionId,
  // l'adresse e-mail de l'utilisateur doit être répétée pour éviter un MITM par votre sevreur applicatif
  authFactor: {
    type: 'EM',
    value: 'user@domain.com'
  },
  challenge: 'challenge envoyé par email',
  twoManRuleKey // `twoManRuleKey` est la clé stockée par votre serveur applicatif pour protéger l'identité de cet utilisateur
})

Votre instance du SDK est alors prête à être utilisée !

WARNING

Il est déconseillé de récupérer la même identité avec ssks2MR.retrieveIdentity sur plusieurs appareils en même temps, au même instant exact, par exemple lors de tests automatisés. Veuillez attendre que l'un des appareils ait fini de récupérer l'identité avant de lancer la récupération sur un autre appareil.

Changement du authFactor d'un utilisateur

Dans le cycle de vie de l'utilisateur dans votre application, celui-ci peut parfois avoir besoin de changer son adresse email, ou son numéro de téléphone.

Si ce facteur d'authentification est utilisé pour stocker son identité Seald sur SSKS, il va falloir également réenregistrer son identité sur cette nouvelle adresse email ou numéro de téléphone.

Pour ce faire, il n'y a pas de point d'API spécifique dans l'API serveur SSKS, ni de fonction spécifique dans le plugin SSKS du SDK. Il suffit en effet de stocker l'identité sur le nouveau facteur (si l'identité n'est pas présente en local, il faudra préalablement la récupérer sur l'ancien facteur), puis d'utiliser l'API serveur pour supprimer l'identité stockée sur l'ancien facteur.

WARNING

Afin de minimiser le risque de perte de l'identité de l'utilisateur, nous recommandons de d'abord stocker l'identité sur le nouveau authFactor avant de la supprimer de l'ancien.

Utilisation d'une TwoManRule Key brute

Dans les appels des fonctions ssks2MR.saveIdentity et ssks2MR.retrieveIdentity, vous pouvez aussi remplacer twoManRuleKey par rawTwoManRuleKey.

Attention, rawTwoManRuleKey doit absolument être un string représentant l'encodage Base64 d'un buffer cryptographiquement aléatoire de 64 octets.

Techniquement, ceci permet d'éviter que twoManRuleKey soit dérivé avec scrypt, ce qui améliore la vitesse d'exécution.

Si vous avez le moindre doute, utilisez plutôt twoManRuleKey.

Utilisation de rawTwoManRuleKey :

js
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKS2MR(keyStorageURL)] })
await seald.initialize()

// Utilisation d'une `rawTwoManRuleKey` avec `ssks2MR.saveIdentity`
await sdk.ssks2MR.saveIdentity({
  userId,
  sessionId,
  authFactor,
  rawTwoManRuleKey: rawKey
})

// Utilisation d'une `rawTwoManRuleKey` avec `ssks2MR.retrieveIdentity`
await sdk.ssks2MR.retrieveIdentity({
  userId,
  sessionId,
  authFactor,
  challenge,
  rawTwoManRuleKey: rawKey
})
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKS2MR(keyStorageURL)] })
await seald.initialize()

// Utilisation d'une `rawTwoManRuleKey` avec `ssks2MR.saveIdentity`
await sdk.ssks2MR.saveIdentity({
  userId,
  sessionId,
  authFactor,
  rawTwoManRuleKey: rawKey
})

// Utilisation d'une `rawTwoManRuleKey` avec `ssks2MR.retrieveIdentity`
await sdk.ssks2MR.retrieveIdentity({
  userId,
  sessionId,
  authFactor,
  challenge,
  rawTwoManRuleKey: rawKey
})

Génération d'une rawTwoManRuleKey adéquate :

js
const crypto = require('crypto')
const util = require('util')
const randomBytes = util.promisify(crypto.randomBytes)

const rawKeyBuffer = await randomBytes(64)
// Ce format est strict : cela doit être exactement 64 octets, encodés en Base64
// et provenir d'une source d'aléa cryptographiquement sûre
const rawKey = rawKeyBuffer.toString('base64')
const crypto = require('crypto')
const util = require('util')
const randomBytes = util.promisify(crypto.randomBytes)

const rawKeyBuffer = await randomBytes(64)
// Ce format est strict : cela doit être exactement 64 octets, encodés en Base64
// et provenir d'une source d'aléa cryptographiquement sûre
const rawKey = rawKeyBuffer.toString('base64')

Export manuel

Il est également possible de gérer le stockage d'une identité manuellement.

Pour ceci, vous pouvez utiliser les fonctions sdk.exportIdentity, et sdk.importIdentity.

sdk.exportIdentity vous permet d'exporter un Buffercontenant l'identité courante de l'instance SDK.

Réciproquement, sdk.importIdentity importe un Buffer d'identité dans l'instance SDK afin de la rendre fonctionnelle.

DANGER

Il est alors à votre charge de vous assurer de la sécurité de ces exports d'identités. Cette sécurité est bien entendue capitale pour assurer la confidentialité des données chiffrées pour cet utilisateur.