Skip to content

Démarrage rapide

Pour démarrer rapidement, nous allons intégrer les fonctionnalités de chiffrement & déchiffrement directement dans le projet exemple, en protégeant l'identité par mot de passe.

La branche sur laquelle est basée cette étape est master, le résultat final est 1-quick-start.

Installation

Dans le front-end, nous allons utiliser la version "web" du SDK @seald-io/sdk avec le module de protection de l'identité par mot de passe @seald-io/sdk-plugin-ssks-password :

shell
cd frontend
npm install --save @seald-io/sdk @seald-io/sdk-plugin-ssks-password
cd frontend
npm install --save @seald-io/sdk @seald-io/sdk-plugin-ssks-password

Sur la partie back-end, nous allons avoir besoin de générer des JSON Web Tokens. Pour ce faire, nous installons le module jose :

shell
cd ../backend
npm install --save jose
cd ../backend
npm install --save jose

Configuration

Pour configurer le SDK, il faut se connecter à votre tableau d'administration et récupérer les 3 éléments suivants :

  • appId : UUID unique pour votre application ;
  • JWT_SHARED_SECRET : secret de JWT ;
  • JWT_SHARED_SECRET_ID : UUID associé au secret correspondant à JWT_SHARED_SECRET utilisée pour générer des JWT.
  • apiURL : URL du serveur d'API à utiliser ;
  • keyStorageURL : URL du serveur de stockage des identités à utiliser.

TIP

Les secrets de JWT peuvent être généré dans les paramètres, onglet secret de JWT.

Pour plus d'information référez-vous à cette documentation.

TIP

Pour créer un compte développeur, suivez les premiers pas.

Pour instancier le SDK, on procède comme suit :

js
import SealdSDK from '@seald-io/sdk'
import SealdSDKPluginSSKSPassword from '@seald-io/sdk-plugin-ssks-password'
/* ... */

let sealdSDKInstance = null

const instantiateSealdSDK = async () => {
  sealdSDKInstance = SealdSDK({
    appId: await getSetting('APP_ID'),
    apiURL: await getSetting('API_URL'),
    plugins: [SealdSDKPluginSSKSPassword(await getSetting('KEY_STORAGE_URL'))]
  })
}

export const getSealdSDKInstance = () => sealdSDKInstance
import SealdSDK from '@seald-io/sdk'
import SealdSDKPluginSSKSPassword from '@seald-io/sdk-plugin-ssks-password'
/* ... */

let sealdSDKInstance = null

const instantiateSealdSDK = async () => {
  sealdSDKInstance = SealdSDK({
    appId: await getSetting('APP_ID'),
    apiURL: await getSetting('API_URL'),
    plugins: [SealdSDKPluginSSKSPassword(await getSetting('KEY_STORAGE_URL'))]
  })
}

export const getSealdSDKInstance = () => sealdSDKInstance

TIP

Étant donné que l'instance du SDK est partagée à travers toute une application en React, il est préférable de la stocker dans une variable dans un fichier de service seald.js et d'exposer un getter plutôt que de l'enregistrer dans une variable immutable.

Gestion des identités Seald

Lors de la création d'une identité Seald, il faut l'associer d'une part au compte développeur, et d'autre part à l'utilisateur (dont l'identifiant est noté userId) dans l'application dans laquelle on intègre le SDK.

Pour cela, il faut générer un JSON Web Token appelé signupJWT à partir du JWT_SECRET, et du JWT_SECRET_ID.

Génération d'un signupJWT

La génération d'un signupJWT se fait par votre backend, et est décrite dans ce guide dédié.

Vous trouverez des exemples de création de signupJWT dans les languages de backend les plus courants dans cette section

Votre backend doit générer un signupJWT lors de l'appel API de création de compte, puis retourner le token dans la réponse de l'appel.

WARNING

En production, il ne faut pas donner au frontend la connaissance de JWT_SHARED_SECRET. Si c'était le cas, elle pourrait être utilisée par un attaquant pour créer de nouveaux comptes associés à votre compte développeur, dépenser vos licences utilisateur, et possiblement réassigner les userIDs de vos utilisateurs à des comptes Seald qu'ils contrôlent.

Vous pouvez vous référer au guide dédié aux JWT.

js
// utils for `generateSignupJWT`
const randomString = (length = 10) => randomBytes(length)
  .then(randomBytes => randomBytes
    .toString('base64')
    .replace(/[^a-z0-9]/gi, '')
    .slice(0, length)
  )

const generateSignupJWT = async (userId) => {
  const token = new jose.SignJWT({
    iss: settings.JWT_SHARED_SECRET_ID,
    jti: random(), // Pour que le JWT ne soit utilisable qu'une seule fois.
    iat: Math.floor(Date.now() / 1000), // Validité limitée à 10 minutes. `Date.now()` donne la date en millisecondes, il faut la date en secondes.
    join_team: true,
    connector_add: {
      value: `${userId}@${settings.APP_ID}`,
      type: 'AP'
    }
  })
    .setProtectedHeader({ alg: 'HS256' })
  return token.sign(Buffer.from(settings.JWT_SHARED_SECRET, 'ascii'))
}

router.post('/', validate(createAccountValidator), async (req, res, next) => {
  /* ... Code de création de compte ... */
  res.json({
    user: user.serialize(),
    signupJWT: await generateSignupJWT(JWT_SHARED_SECRET, JWT_SHARED_SECRET_ID, APP_ID, userId)
  })
})
// utils for `generateSignupJWT`
const randomString = (length = 10) => randomBytes(length)
  .then(randomBytes => randomBytes
    .toString('base64')
    .replace(/[^a-z0-9]/gi, '')
    .slice(0, length)
  )

const generateSignupJWT = async (userId) => {
  const token = new jose.SignJWT({
    iss: settings.JWT_SHARED_SECRET_ID,
    jti: random(), // Pour que le JWT ne soit utilisable qu'une seule fois.
    iat: Math.floor(Date.now() / 1000), // Validité limitée à 10 minutes. `Date.now()` donne la date en millisecondes, il faut la date en secondes.
    join_team: true,
    connector_add: {
      value: `${userId}@${settings.APP_ID}`,
      type: 'AP'
    }
  })
    .setProtectedHeader({ alg: 'HS256' })
  return token.sign(Buffer.from(settings.JWT_SHARED_SECRET, 'ascii'))
}

router.post('/', validate(createAccountValidator), async (req, res, next) => {
  /* ... Code de création de compte ... */
  res.json({
    user: user.serialize(),
    signupJWT: await generateSignupJWT(JWT_SHARED_SECRET, JWT_SHARED_SECRET_ID, APP_ID, userId)
  })
})
js
// Les informations sont récuperées lors de l'appel API de création de compte.
const { user: { id }, signupJWT } = await apiClient.rest.account.create({
  emailAddress,
  password,
  name
})
// Les informations sont récuperées lors de l'appel API de création de compte.
const { user: { id }, signupJWT } = await apiClient.rest.account.create({
  emailAddress,
  password,
  name
})

Création d'une identité

Une fois le jeton généré, on peut créer une nouvelle identité de la façon suivante :

js
const accountInfo = await sealdSDKInstance.initiateIdentity({ signupJWT })
const accountInfo = await sealdSDKInstance.initiateIdentity({ signupJWT })

La fonction retourne un object de type accountInfo. Cet objet contient des informations sur le compte nouvellement créé, notamment un identifiant appelé sealdId. Vous aurez besoin de sauvegarder cet identifiant et de l'associer à l'utilisateur sur votre back-end, et dans le front-end.

Les fonctions suivantes permettent de sauvegarder cet identifiant dans le model front-end et back-end.

js
router.post('/sealdId', validate(sealdIdValidator), async (req, res, next) => {
  try {
    const { sealdId } = req.body
    const userId = req.session.user.id
    const user = await User.findOne({ where: { id: userId } })
    await user.setSealdId(sealdId)
    global.io.emit('user:created', user.serialize())
    res.json({ sealdId })
  } catch (error) {
    next(error)
  }
})
router.post('/sealdId', validate(sealdIdValidator), async (req, res, next) => {
  try {
    const { sealdId } = req.body
    const userId = req.session.user.id
    const user = await User.findOne({ where: { id: userId } })
    await user.setSealdId(sealdId)
    global.io.emit('user:created', user.serialize())
    res.json({ sealdId })
  } catch (error) {
    next(error)
  }
})
js
await currentUser.setSealdId(sealdId)
await currentUser.setSealdId(sealdId)

Une fois cet identifiant enregistré, l'instance du SDK est prête pour chiffrer et déchiffrer, et les clés propres à l'identité sont en mémoire.

Protection de l'identité par mot de passe

Une fois l'identité créée, elle n'existe qu'en mémoire, il faut la sauvegarder pour pouvoir l'utiliser dans une session ultérieure.

Pour cela nous allons utiliser le module @seald-io/sdk-plugin-ssks-password qui permet de protéger les clés d'identité par un mot de passe password.

js
await sealdSDKInstance.ssksPassword.saveIdentity({
  userId,
  password
})
await sealdSDKInstance.ssksPassword.saveIdentity({
  userId,
  password
})

Une fois cette fonction exécutée, l'identité est sauvegardée et pourra être récupérée en utilisant la fonction équivalente sealdSDKInstance.ssksPassword.retrieveIdentity depuis une instance nouvellement instanciée.

Pour plus de détails sur ce mécanisme, référez-vous au guide dédié à la protection des identités par mot de passe.

TIP

Pour une exécution plus rapide, dans le cadre d'une utilisation avancée du SDK, vous pouvez personnaliser la dérivation du mot de passe en une rawEncryptionKey et une rawStorageKey, et passer celles-ci à la place du mot de passe à ssksPassword.saveIdentity. Pour plus de détails, allez voir le paragraphe "Personnaliser la dérivation de mot de passe" du guide sur les identités.

Exposer des fonctions createIdentity et retrieveIdentity

Si on rassemble tous les éléments étudiés dans les parties précédentes, on peut exposer deux fonctions :

js
export const createIdentity = async ({ userId, password, signupJWT }) => {
  await instantiateSealdSDK()
  const accountInfo = await sealdSDKInstance.initiateIdentity({ signupJWT })
  await sealdSDKInstance.ssksPassword.saveIdentity({ userId, password })
  return accountInfo.sealdId
}

export const retrieveIdentity = async ({ userId, password }) => {
  await instantiateSealdSDK()
  return await sealdSDKInstance.ssksPassword.retrieveIdentity({ userId, password })
}
export const createIdentity = async ({ userId, password, signupJWT }) => {
  await instantiateSealdSDK()
  const accountInfo = await sealdSDKInstance.initiateIdentity({ signupJWT })
  await sealdSDKInstance.ssksPassword.saveIdentity({ userId, password })
  return accountInfo.sealdId
}

export const retrieveIdentity = async ({ userId, password }) => {
  await instantiateSealdSDK()
  return await sealdSDKInstance.ssksPassword.retrieveIdentity({ userId, password })
}

Intégrer à la création de compte et à la connexion

L'étape suivante consiste à appeler ces fonctions lors de la création du compte dans l'application et lors de la connexion.

Pour la créer lors de la création de compte :

js
/* frontend/src/containers/SignUp.jsx */
import { createIdentity } from '../services/seald'

/* ... */
const currentUser = await User.createAccount({ emailAddress, password, name })
// Après la création du compte sur le back-end, créons l'identité Seald
const sealdId = await createIdentity({ userId: currentUser.id, password, signupJWT: currentUser.signupJWT }) // TODO: Ne pas utiliser le mot de passe en clair à la fois pour la création de compte et la protection de l'identité
// Une fois l'identité seald créée, associons le `sealdId` avec le compte utilisateur
await currentUser.setSealdId(sealdId)
/* ... */
/* frontend/src/containers/SignUp.jsx */
import { createIdentity } from '../services/seald'

/* ... */
const currentUser = await User.createAccount({ emailAddress, password, name })
// Après la création du compte sur le back-end, créons l'identité Seald
const sealdId = await createIdentity({ userId: currentUser.id, password, signupJWT: currentUser.signupJWT }) // TODO: Ne pas utiliser le mot de passe en clair à la fois pour la création de compte et la protection de l'identité
// Une fois l'identité seald créée, associons le `sealdId` avec le compte utilisateur
await currentUser.setSealdId(sealdId)
/* ... */

Pour la récupérer à la connexion :

js
/* frontend/src/containers/SignIn.jsx */
import { retrieveIdentity } from '../services/seald'

/* ... */
const currentUser = await User.login({ emailAddress, password })
// right after the account is logged in on the back-end, let's retrieve the Seald identity
const { sealdId } = await await retrieveIdentity({ userId: currentUser.id, password }) // TODO: Ne pas utiliser le mot de passe en clair à la fois pour la création de compte et la protection de l'identité
/* ... */
/* frontend/src/containers/SignIn.jsx */
import { retrieveIdentity } from '../services/seald'

/* ... */
const currentUser = await User.login({ emailAddress, password })
// right after the account is logged in on the back-end, let's retrieve the Seald identity
const { sealdId } = await await retrieveIdentity({ userId: currentUser.id, password }) // TODO: Ne pas utiliser le mot de passe en clair à la fois pour la création de compte et la protection de l'identité
/* ... */

Comme l'identité n'est conservée qu'en mémoire dans le navigateur, si vous ouvrez un nouvel onglet, vous devrez retaper le mot de passe. Pour contourner ce problème, nous n'essayons pas d'obtenir du serveur le profil de l'utilisateur connecté :

js
/* frontend/src/App.js */
/* ... */
const currentUser = User.getCurrentUser() // || await User.updateCurrentUser() // TODO : Seald-SDK est chargé en mémoire, il n'est pas possible de le récuperer la session, il faut taper le mot de passe.
/* ... */
/* frontend/src/App.js */
/* ... */
const currentUser = User.getCurrentUser() // || await User.updateCurrentUser() // TODO : Seald-SDK est chargé en mémoire, il n'est pas possible de le récuperer la session, il faut taper le mot de passe.
/* ... */

Nous verrons plus tard comment faire persister cette identité dans le navigateur.

DANGER

Nous mettons ici de côté un souci majeur de sécurité : le mot de passe d'authentification ne doit pas être utilisé tel quel pour protéger l'identité. Il faut modifier la méthode d'authentification (avec une pré-dérivation du mot de passe) pour que le serveur applicatif ne puisse pas avoir connaissance du mot de passe. Un guide dédié est disponible ici.

Chiffrer & déchiffrer des messages

Pour effectuer des chiffrements et déchiffrements de messages, il faut :

  • créer une session de chiffrement partagée entre les destinataires ;
  • chiffrer les messages avant envoi ;
  • déchiffrer les messages après réception.

Session de chiffrement

Pour créer une session de chiffrement partagée entre les utilisateurs userId_1 et userId_2 dans le salon de chat roomId_1, on procède de la façon suivante :

js
/* frontend/src/components/Chat.jsx */
import { getSealdSDKInstance } from '../services/seald'

const sealdIds = ['sealdId_1', 'sealdId_2']
const metadata = 'roomId_1'
const session = await getSealdSDKInstance().createEncryptionSession({ sealdIds }, { metadata })
/* frontend/src/components/Chat.jsx */
import { getSealdSDKInstance } from '../services/seald'

const sealdIds = ['sealdId_1', 'sealdId_2']
const metadata = 'roomId_1'
const session = await getSealdSDKInstance().createEncryptionSession({ sealdIds }, { metadata })

Cela renvoie une EncryptionSession avec un nouveau sessionId unique attribué par le serveur.

Dans le cadre de ce projet, on place dans metadata l'identifiant du salon de chat roomId.

Pour chiffrer un message avec une session, on procède comme suit :

js
/* frontend/src/components/Chat.jsx */
const clearText = 'hello world !'
const encryptedMessage = await session.encryptMessage(clearText)
/* frontend/src/components/Chat.jsx */
const clearText = 'hello world !'
const encryptedMessage = await session.encryptMessage(clearText)

Cela va donner un string de la forme :

js
'{"sessionId":"0000000000000000000000","data":"8RwaOppCD3uIJVFv2LoP3XGXpomj0xsMv4qmMVy30Vdqor2w0+DVCXu3j13PEyN2KfJm6SiSrWDRMDziiiOUjQ=="}'
'{"sessionId":"0000000000000000000000","data":"8RwaOppCD3uIJVFv2LoP3XGXpomj0xsMv4qmMVy30Vdqor2w0+DVCXu3j13PEyN2KfJm6SiSrWDRMDziiiOUjQ=="}'

On peut récupérer une instance de la session, soit avec le sessionId, soit directement avec le message chiffré :

js
const retrievedSession = await getSealdSDKInstance().retrieveEncryptionSession({
  encryptedMessage,
  /* OR */
  sessionId: '0000000000000000000000'
})
const retrievedSession = await getSealdSDKInstance().retrieveEncryptionSession({
  encryptedMessage,
  /* OR */
  sessionId: '0000000000000000000000'
})

Une fois la session récupérée, on peut l'utiliser pour déchiffrer un message :

js
const decryptedMessage = await retrievedSession.decryptMessage(encryptedMessage)
const decryptedMessage = await retrievedSession.decryptMessage(encryptedMessage)

On peut aussi ajouter / révoquer des destinataires de la session :

javascript
await session.addRecipients({ sealdIds: [user3SealdId] })

await session.revokeRecipients({ sealdIds: [user3SealdId] })
await session.addRecipients({ sealdIds: [user3SealdId] })

await session.revokeRecipients({ sealdIds: [user3SealdId] })

Pour plus de détails sur les sessions, vous pouvez consulter le guide dédié.

Intégration dans un salon de chat

Pour intégrer ces fonctions dans le chat, il faut modifier trois éléments :

  • l'envoi des messages, qui doit désormais chiffrer pour les destinataires du salon ;
  • la réception de messages, qui doit désormais déchiffrer ;
  • la gestion des membres d'un salon, qui doit désormais transposer les membres d'un salon en droits cryptographiques.

Chiffrement à l'envoi

Il faut appeler la fonction encrypt à chaque fois qu'un message est posté.

Dans le fichier Chat.jsx se trouve une fonction handleSumbitMessage que l'on va modifier pour y incorporer du chiffrement :

js
/* frontend/src/components/Chat.jsx */
import { useRef } from 'react'
/* ... */
function Chat ({ roomId: currentRoomId }) {
  /* ... */

  const sealdSessionRef = useRef(null)
  /* ... */
  const handleSubmitMessage = async e => {
    e.preventDefault()
    if (!state.room) {
      enqueueSnackbar('Please select a room or create a new one', { variant: 'error' })
    } else if (state.message.trim()) {
      try {
        // If a session is not known for this chat session, let's create one:
        if (!sealdSessionRef.current) {
          sealdSessionRef.current = await getSealdSDKInstance().createEncryptionSession({ sealdIds: state.room.users.map(u => u.sealdId) }, { metadata: state.room.id })
        }
        // Use this session to encrypt the message
        const encryptedMessage = await sealdSessionRef.current.encryptMessage(state.message)

        // Post the `encryptedMessage` instead of the `message`
        await state.room.postMessage(encryptedMessage)

        setState(draft => {
          draft.message = ''
        })
      } catch (error) {
        console.error(error)
        enqueueSnackbar(error.message, { variant: 'error' })
      }
    }
  }
}
/* frontend/src/components/Chat.jsx */
import { useRef } from 'react'
/* ... */
function Chat ({ roomId: currentRoomId }) {
  /* ... */

  const sealdSessionRef = useRef(null)
  /* ... */
  const handleSubmitMessage = async e => {
    e.preventDefault()
    if (!state.room) {
      enqueueSnackbar('Please select a room or create a new one', { variant: 'error' })
    } else if (state.message.trim()) {
      try {
        // If a session is not known for this chat session, let's create one:
        if (!sealdSessionRef.current) {
          sealdSessionRef.current = await getSealdSDKInstance().createEncryptionSession({ sealdIds: state.room.users.map(u => u.sealdId) }, { metadata: state.room.id })
        }
        // Use this session to encrypt the message
        const encryptedMessage = await sealdSessionRef.current.encryptMessage(state.message)

        // Post the `encryptedMessage` instead of the `message`
        await state.room.postMessage(encryptedMessage)

        setState(draft => {
          draft.message = ''
        })
      } catch (error) {
        console.error(error)
        enqueueSnackbar(error.message, { variant: 'error' })
      }
    }
  }
}

Par ailleurs, lors de la création d'un salon de chat multi-utilisateurs dans ManageDialogRoom.jsx un message 'Hello 👋'est envoyé. Il faut le chiffrer :

js
/* frontend/src/components/ManageDialogRoom.jsx */

/* ... */

const newRoom = await Room.create(
  dialogRoom.name,
  dialogRoom.selectedUsers
)
const sealdSession = await getSealdSDKInstance().createEncryptionSession({ sealdIds: dialogRoom.selectedUsers.map(u => u.sealdId) }, { metadata: newRoom.id })
await newRoom.postMessage(await sealdSession.encryptMessage('Hello 👋'))

/* ... */
/* frontend/src/components/ManageDialogRoom.jsx */

/* ... */

const newRoom = await Room.create(
  dialogRoom.name,
  dialogRoom.selectedUsers
)
const sealdSession = await getSealdSDKInstance().createEncryptionSession({ sealdIds: dialogRoom.selectedUsers.map(u => u.sealdId) }, { metadata: newRoom.id })
await newRoom.postMessage(await sealdSession.encryptMessage('Hello 👋'))

/* ... */

Déchiffrement à la réception

Il faut appeler la fonction decrypt à chaque fois qu'un message est collecté.

Pour cela on va créer quelques fonctions helpers dans Chat.jsx qui serviront à utiliser le même modèle de données partout :

js
const decrypt = async m => ({
  ...m,
  message: await sealdSessionRef.current.decryptMessage(m.encryptedMessage)
})

const serializeMessage = m => ({
  encryptedMessage: m.content,
  timestamp: m.createdAt,
  senderId: m.senderId,
  id: m.id
})
const decrypt = async m => ({
  ...m,
  message: await sealdSessionRef.current.decryptMessage(m.encryptedMessage)
})

const serializeMessage = m => ({
  encryptedMessage: m.content,
  timestamp: m.createdAt,
  senderId: m.senderId,
  id: m.id
})

On va modifier l'eventListener de l'évènement room:messageSent :

js
/* frontend/src/components/Chat.jsx */
useEffect(() => {
  const listener = async payload => {
    const { id, roomId } = payload
    if (currentRoomId === roomId) {
      const message = serializeMessage(payload)
      if (!sealdSessionRef.current) sealdSessionRef.current = await getSealdSDKInstance().retrieveEncryptionSession({ encryptedMessage: message.encryptedMessage })

      const clearMessage = await decrypt(message)

      setState(draft => {
        if (!draft.messages.find(m => m.id === id)) {
          draft.messages = [...draft.messages, clearMessage]
        }
      })
    }
  }
  if (socket) socket.on('room:messageSent', listener)
  return () => { if (socket) socket.off('room:messageSent', listener) }
}, [socket, currentRoomId])
/* frontend/src/components/Chat.jsx */
useEffect(() => {
  const listener = async payload => {
    const { id, roomId } = payload
    if (currentRoomId === roomId) {
      const message = serializeMessage(payload)
      if (!sealdSessionRef.current) sealdSessionRef.current = await getSealdSDKInstance().retrieveEncryptionSession({ encryptedMessage: message.encryptedMessage })

      const clearMessage = await decrypt(message)

      setState(draft => {
        if (!draft.messages.find(m => m.id === id)) {
          draft.messages = [...draft.messages, clearMessage]
        }
      })
    }
  }
  if (socket) socket.on('room:messageSent', listener)
  return () => { if (socket) socket.off('room:messageSent', listener) }
}, [socket, currentRoomId])

On va modifier la récupération de l'historique des messages au chargement d'un salon :

js
/* frontend/src/components/Chat.jsx */
const init = async () => {
  // get current room (unchanged)
  const currentRoom = rooms.find(r => r.id === currentRoomId)
  if (currentRoom) {
    try {
      if (!(currentRoom.users.includes(currentUser.id))) {
        /* handle error */
      } else {
        // set initial state (unchanged)
        setState(draft => {
          draft.message = ''
          draft.isCustomRoom = !currentRoom.one2one
          draft.room = currentRoom
          draft.roomTitle = currentRoom.one2one ? users.find(user => currentRoom.users.includes(user.id) && user.id !== currentUser.id)?.name : currentRoom.name
          draft.canCustomizeRoom = !currentRoom.one2one && currentRoom.ownerId === currentUser.id
          draft.users = users.filter(u => currentRoom.users.includes(u.id))
          draft.messages = []
        })
        // Reset stored session when re-initializing
        sealdSessionRef.current = null
        // Get encrypted messages
        const messages = (await currentRoom.getMessages()).map(serializeMessage)
        const clearMessages = []
        if (messages.length) {
          // Retrieve session using the first encryptedMessage
          sealdSessionRef.current = await getSealdSDKInstance().retrieveEncryptionSession({ encryptedMessage: messages[0].encryptedMessage })
          // Decrypt each message
          clearMessages.push(...(await Promise.all(messages.map(decrypt))))
        }
        // Set state
        setState(draft => {
          draft.messages = clearMessages
          draft.isLoading = false
        })
      }
    } catch (error) {
      /* handle error */
    }
  } else {
    /* handle error */
  }
}
/* frontend/src/components/Chat.jsx */
const init = async () => {
  // get current room (unchanged)
  const currentRoom = rooms.find(r => r.id === currentRoomId)
  if (currentRoom) {
    try {
      if (!(currentRoom.users.includes(currentUser.id))) {
        /* handle error */
      } else {
        // set initial state (unchanged)
        setState(draft => {
          draft.message = ''
          draft.isCustomRoom = !currentRoom.one2one
          draft.room = currentRoom
          draft.roomTitle = currentRoom.one2one ? users.find(user => currentRoom.users.includes(user.id) && user.id !== currentUser.id)?.name : currentRoom.name
          draft.canCustomizeRoom = !currentRoom.one2one && currentRoom.ownerId === currentUser.id
          draft.users = users.filter(u => currentRoom.users.includes(u.id))
          draft.messages = []
        })
        // Reset stored session when re-initializing
        sealdSessionRef.current = null
        // Get encrypted messages
        const messages = (await currentRoom.getMessages()).map(serializeMessage)
        const clearMessages = []
        if (messages.length) {
          // Retrieve session using the first encryptedMessage
          sealdSessionRef.current = await getSealdSDKInstance().retrieveEncryptionSession({ encryptedMessage: messages[0].encryptedMessage })
          // Decrypt each message
          clearMessages.push(...(await Promise.all(messages.map(decrypt))))
        }
        // Set state
        setState(draft => {
          draft.messages = clearMessages
          draft.isLoading = false
        })
      }
    } catch (error) {
      /* handle error */
    }
  } else {
    /* handle error */
  }
}

Modifier les droits à l'édition d'un salon

Lorsque le salon est multi-utilisateurs, le créateur du salon peut ajouter / supprimer des membres. Les sessions permettent de gérer ces mouvements.

Pour cela il faut au moment où le créateur du salon effectue ces mouvements sur le salon, effectuer les mêmes mouvements sur la session de chiffrement.

Lorsque l'on déclenche la modification d'un salon, il faut donner la session associée au salon lors du dispatch :

js
/* frontend/src/components/Chat.jsx */
const handleEditRoom = () => {
  dispatch({
    type: START_EDIT_DIALOG_ROOM,
    payload: {
      room: state.room,
      name: state.room.name,
      selectedUsersId: state.room.users,
      sealdSession: sealdSessionRef.current
    }
  })
}
/* frontend/src/components/Chat.jsx */
const handleEditRoom = () => {
  dispatch({
    type: START_EDIT_DIALOG_ROOM,
    payload: {
      room: state.room,
      name: state.room.name,
      selectedUsersId: state.room.users,
      sealdSession: sealdSessionRef.current
    }
  })
}

Dans ManageDialogRoom.jsx avant de faire l'appel API au serveur pour effectivement éditer le salon :

js
/* frontend/src/components/ManageDialogRoom.jsx  */
if (dialogRoom.room) {
  await dialogRoom.sealdSession.revokeRecipients({ sealdIds: dialogRoom.room.users.filter(u => !dialogRoom.selectedUsers.includes(u.id)).map(u => u.sealdId) })
  await dialogRoom.sealdSession.addRecipients({ sealdIds: dialogRoom.selectedUsers.filter(u => !dialogRoom.room.users.includes(u.id)).map(u => u.sealdId) })
  await dialogRoom.room.edit({ name: dialogRoom.name, users: dialogRoom.selectedUsers })
}
/* frontend/src/components/ManageDialogRoom.jsx  */
if (dialogRoom.room) {
  await dialogRoom.sealdSession.revokeRecipients({ sealdIds: dialogRoom.room.users.filter(u => !dialogRoom.selectedUsers.includes(u.id)).map(u => u.sealdId) })
  await dialogRoom.sealdSession.addRecipients({ sealdIds: dialogRoom.selectedUsers.filter(u => !dialogRoom.room.users.includes(u.id)).map(u => u.sealdId) })
  await dialogRoom.room.edit({ name: dialogRoom.name, users: dialogRoom.selectedUsers })
}

Conclusion

Nous avons pu intégrer rapidement du chiffrement dans ce projet, restent en suspens 3 sujets avant de passer en production :

  • la pré-dérivation du mot de passe ;
  • la persistance de l'identité à l'ouverture d'un nouvel onglet en stockant l'identité en localstorage ;
  • la génération des jetons de licence depuis le backend.