Skip to content

Sessions de chiffrement

Lorsque vous comptez chiffrer de nombreux messages à la suite, par exemple dans un cas de discussion instantanée, vous pouvez utiliser une session de chiffrement. Une session de chiffrement utilise une même clé pour chiffrer tous les messages de la session.

Les avantages sont que votre code d'intégration peut être plus simple, et que cela diminue le nombre de requêtes réseau liées à la récupération de clés de déchiffrement.

L'inconvénient est que vous perdez la granularité offerte par le fait de chiffrer chaque message indépendamment : vous ne pouvez pas ajouter / révoquer un destinataire sur un seul message de la session, mais seulement sur la session toute entière.

Créer une session de chiffrement

Vous pouvez créer une session avec la fonction sealdSDK.createEncryptionSession.

Lors de la création, deux éléments sont à définir :

  • sealdIds : un tableau des identifiants Seald des utilisateurs de votre application qui seront autorisés pour cette session de chiffrement ;
  • metadata : argument optionnel déclarant une métadonnée non chiffrée à l'API Seald. Elle peut servir dans l'administration d'un compte Seald.
javascript
const session = await seald.createEncryptionSession(
  {
    sealdIds: [mySealdId, user2SealdId] // destinataires : utilisateurs autorisés à déchiffrer les messages de la session
  },
  {
    metadata: 'Ma session de demo'
  }
)

La session ainsi créée est de type EncryptionSession. Elle possède un attribut sessionId, et plusieurs méthodes.

La personne qui crée une session en est alors l'administrateur : par défaut, seul cet utilisateur sera en mesure de révoquer la session.

TIP

Si vous créez une session pour un groupe dont l'utilisateur fait partie, vous pouvez utiliser l'argument optionnelencryptForSelf: false afin de ne pas chiffrer directement pour ses propres identités. L'utilisateur pourra toujours déchiffrer les données grâce à son appartenance au groupe. Ceci peut vous permettre d'améliorer la performance du chiffrement.

Chiffrer des messages

Avec l'objet EncryptionSession, on peut dès lors chiffrer des messages, qui seront lisibles par tous les destinataires de la session.

javascript
const encryptedMessage = await session.encryptMessage('Super secret message') // chiffrement d'un message à l'aide de la session

Cela va donner un string de la forme :

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

Celui-ci contient le sessionId qui correspond à l'identifiant de la session de chiffrement et data qui correspond à la donnée chiffrée.

Vous pouvez également utiliser l'option raw pour obtenir uniquement le message chiffré brut, sans le sessionId :

javascript
const rawEncryptedMessage = await session.encryptMessage('Super secret message', { raw: true }) // chiffrement d'un message au format brut

Cela va donner un string de la forme :

javascript
'8RwaOppCD3uIJVFv2LoP3XGXpomj0xsMv4qmMVy30Vdqor2w0+DVCXu3j13PEyN2KfJm6SiSrWDRMDziiiOUjQ=='

WARNING

Un message chiffré au format brut ne permet pas de récupérer la session correspondante, il est donc de votre ressort d'enregistrer le sessionId séparément pour pouvoir récupérer la session.

Récupération d'une session

Une fois une session créée et un message chiffré, il faut être en mesure de récupérer la session a posteriori, y compris par l'émetteur lui-même.

Pour cela, il y a deux possibilités :

  • à partir du sessionId, mais cela nécessite de le stocker dans l'application dans un champ à part ;
  • à partir d'un message chiffré (excepté au format brut) avec cette session directement, ce qui a le mérite d'être plus simple.

TIP

Vous pouvez récupérer le sessionId à partir de l'objet de session avec sa propriété session.sessionId.

Dans les deux cas, on utilise la méthode sealdSDK.retrieveEncryptionSession, soit avec l'argument nommé sessionId si on le connait, soit avec l'argument nommé encryptedMessage :

javascript
const sessionFromId = await seald.retrieveEncryptionSession({ sessionId }) // récupérer une session avec le sessionId

const sessionFromMessage = await seald.retrieveEncryptionSession({ encryptedMessage }) // récupérer une session avec un message chiffré dans la session

Déchiffrement

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

javascript
const decryptedMessage = await session.decryptMessage(encryptedMessage) // déchiffrement d'un message à l'aide de la session

Ajouter / révoquer des destinataires de la session

Tout destinataire de la session ayant le droit 'forward' (ce qui est le cas par défaut) peut y ajouter d'autres destinataires, grâce à la méthode session.addRecipients.

javascript
await session.addRecipients({ sealdIds: [user3SealdId] }) // nécessite le droit `'forward'`

Pour révoquer un destinataire de la session avec la méthode session.revokeRecipients, il faut avoir le droit 'revoke'. Par défaut, seule le créateur de la session possède ce droit.

Ainsi, imaginons un scénario où user1 a ajouté user2 dans la session, et user2 a rajouté user3.

  • user3 ne peut révoquer personne
  • user2 ne peut révoquer personne
  • user1 peut révoquer n'importe qui
javascript
await session.revokeRecipients({ sealdIds: [user3SealdId] }) // nécessite le droit `'revoke'`

Un utilisateur ayant le droit 'revoke' (par défaut, seulement le créateur de la session) peut également utiliser la methode session.revoke pour révoquer entièrement la session et tout son contenu.

javascript
await session.revoke() // nécessite le droit `'revoke'`

Chiffrer & déchiffrer des fichiers

Une EncryptionSession expose également deux méthodes encryptFile et decryptFile permettant d'utiliser cette session de chiffrement pour chiffrer des fichiers.

Comme expliqué dans le guide dédié au chiffrement des fichiers, ces méthodes supportent différents types (string, Buffer, Blob dans le navigateur uniquement, ReadableStream des Web Streams, Readable des Streams Node.JS).

Différence avec SDK#encryptFile et SDK#decryptFile

En utilisant une EncryptionSession pour chiffrer / déchiffrer des fichiers, on peut réutiliser la clé de chiffrement de la session sur différents fichiers / messages, et on gère les destinataires au niveau de la session de chiffrement.

Ainsi la méthode encryptFile d'une EncryptionSession ne prend pas l'argument recipients, et a seulement les arguments suivants :

  • clearFile : même chose que SDK#encryptFile ;
  • filename : dans ce cas, il n'est pas utilisé comme metadata, mais seulement comme nom de fichier dans le tar chiffré (voir le format d'un fichier chiffré) ;
  • options : l'objet options dont seule la propriété fileSize est utilisée, et qui n'est nécessaire que lorsque clearFile est de type Readable ou ReadableStream.

Chiffrement

Voici un exemple de chiffrement (avec le type Buffer) :

js
const session = await seald.createEncryptionSession(
  {
    sealdIds: [mySealdId, user2SealdId] // destinataires : utilisateurs autorisés à déchiffrer les messages de la session
  },
  {
    metadata: 'Ma session de demo'
  }
)

const encryptedBuffer = await session.encryptFile(
  Buffer.from('Secret file content', 'utf8'),
  'SecretFile.txt'
)

Déchiffrement

Pour déchiffrer un fichier chiffré avec une session, on doit utiliser la même EncryptionSession (préalablement récupérée avec SDK#retrieveEncryptionSession):

js
const {
  data, // An instance of Buffer containing the clear text data
  type, // 'buffer'
  size, // 19
  sessionId, // the encryption session ID
  filename // 'SecretFile.txt'
} = await session.decryptFile(encryptedBuffer)

console.log(data.toString('utf8')) // This will log 'Secret file content'

On peut également directement utiliser SDK#decryptFile qui récupèrera automatiquement l'EncryptionSession associée (l'ID de l'EncryptionSession est toujours inclus dans le résultat d'un chiffrement de fichiers) :

js
const {
  data, // An instance of Buffer containing the clear text data
  type, // 'buffer'
  size, // 19
  sessionId, // the encryption session ID
  filename // 'SecretFile.txt'
} = await seald.decryptFile(encryptedBuffer)

console.log(data.toString('utf8')) // This will log 'Secret file content'

WARNING

Utiliser la méthode decryptFile d'une EncryptionSession sur un fichier chiffré pour une autre session ne fonctionnera pas.

TIP

Pour avoir des exemples avec d'autres types, référez-vous à la section d'exemples du guide sur le chiffrement de fichiers. Vous devrez simplement prendre en compte les différences sus-mentionnés.

Mise en cache des sessions de chiffrement

Activation de la mise en cache

Pour minimiser les appels réseau lors des déchiffrement, vous pouvez mettre en cache les sessions de chiffrement.

Afin d'activer cette mise en cache, vous devez mettre une valeur à l'argument encryptionSessionCacheTTL lors de l'initialisation du SDK. Cet argument définit la durée, en millisecondes, pendant laquelle ce cache est gardé valide (avec la valeur -1 pour une conservation sans expiration du cache). Par défaut, encryptionSessionCacheTTL est à 0, ce qui désactive l'enregistrement des sessions de chiffrement dans le cache.

js
const seald = SealdSDK({
  appId,
  apiURL,
  encryptionSessionCacheTTL: 30 * 60 * 1000 // garder le cache valide pendant 30 min
})

Ce cache sera automatiquement rempli à la création ou à la récupération d'une session de chiffrement, et sera automatiquement utilisé lors de la récupération d'une session de chiffrement. Vous pouvez, lors des appels à ces fonctions, décider de modifier ce comportement avec l'argument useCache: false.

js
// récupèrera la session dans le cache, sans appel réseau (si le cache a été
// activé à l'initialisation du SDK avec un `encryptionSessionCacheTTL` non nul,
// et si ce `sessionId` est déjà dans le cache)
const sessionWithCache = seald.retrieveEncryptionSession({ sessionId })

// récupèrera la session en appelant les serveurs Seald
const sessionWithoutCache = seald.retrieveEncryptionSession({ sessionId, useCache: false })

Nettoyage du cache

Par défaut, le cache est nettoyé automatiquement, avec un intervalle égal au encryptionSessionCacheTTL, mais avec un temps minimum de 10 secondes. Le nettoyage supprime du stockage les entrées du cache expirées (plus vielles que encryptionSessionCacheTTL). Ceci signifie que, dans cette configuration par défaut et dans le pire des cas, une entrée pourrait rester stockée jusqu'a deux fois la durée de encryptionSessionCacheTTL.

Vous pouvez aussi changer cet intervalle de nettoyage automatique, avec l'argument encryptionSessionCacheCleanupInterval. Cet argument définit l'intervalle, en millisecondes, entre deux nettoyages automatiques du cache (avec la valeur -1 pour désactiver le nettoyage automatique).

js
const seald = SealdSDK({
  appId,
  apiURL,
  encryptionSessionCacheTTL: 30 * 60 * 1000, // garder le cache valide pendant 30 min
  encryptionSessionCacheCleanupInterval: 10 * 60 * 1000 // nettoyer automatiquement les entrées expirées toutes les 10 min
})

Vous pouvez également lancer manuellement un nettoyage du cache, avec la fonction sdk.utils.cleanCache :

js
await sdk.utils.cleanCache()

Personnalisation du stockage du cache

Par défaut, ce cache est seulement en mémoire, spécifique à chaque instance du SDK. Si vous voulez un cache persistant, vous pouvez personnaliser la méthode de stockage du cache, avec l'argument createEncryptionSessionCache lors de l'initialisation du SDK.

Cache dans le localStorage

Dans le navigateur, le cas d'usage le plus probable est de vouloir utiliser le localStorage afin d'avoir un cache persistant entre les chargements de pages et entre les onglets. La version pour navigateur du SDK contient une implémentation de createEncryptionSessionCache qui stocke les données en localStorage, afin de vous éviter d'implémenter ce cas d'usage courant vous-mêmes. Celle-ci se nomme createLocalStorageEncryptionSessionCache, et chiffre les entrées du cache avec la même clé que votre base de données avant de les stocker dans le localStorage.

Ce createLocalStorageEncryptionSessionCache est disponible en tant qu'import nommé du SDK, dans sa version navigateur uniquement. Puisqu'elle stocke le cache chiffré avec les mêmes clés que que votre base de données locale, elle ne peut être utilisée qu'avec une base de données locale persistante.

js
// Dans le navigateur uniquement
import SealdSDK, { createLocalStorageEncryptionSessionCache } from '@seald-io/sdk'

const seald = SealdSDK({
  appId,
  apiURL,
  databasePath,
  databaseKey,
  encryptionSessionCacheTTL: 30 * 60 * 1000, // garder le cache valide pendant 30 min
  createEncryptionSessionCache: createLocalStorageEncryptionSessionCache
})

createLocalStorageEncryptionSessionCache est également disponible en tant que propriété du constructeur SealdSDK, dans sa version navigateur uniquement.

js
// Dans le navigateur uniquement
const seald = SealdSDK({
  appId,
  apiURL,
  encryptionSessionCacheTTL: 30 * 60 * 1000, // garder le cache valide pendant 30 min
  createEncryptionSessionCache: SealdSDK.createLocalStorageEncryptionSessionCache
})

Cache manuel

Vous pouvez également faire votre propre implémentation de createEncryptionSessionCache.

Cet argument prend une fonction, qui reçoit plusieurs arguments qui pourront vous aider dans votre implémentation. Il doit rendre un objet qui servira de cache, EncryptionSessionCache.

Cet objet doit posséder quatre méthodes :

  • set (id: string, value: { sessionSymKey: string, serializationDate: number }): void : prend un id et un objet, et doit stocker celui-ci.
  • get (id: string, dateOnly?: boolean): { sessionSymKey: string|null, serializationDate: number } : prend un id et doit rendre l'objet éventuellement précédemment stocké avec cet id, ou null. Si l'argument dateOnly est passé à true, cette fonction peut ne rendre que serializationDate dans l'objet rendu, avec sessionSymKey mis à null.
  • keys(): Array<string> : doit rendre la liste des ids des entrées actuellement stockées.
  • delete(id: string): void : doit supprimer l'entrée de cache correspondant à l'id passé.

Toutes ces fonctions (createEncryptionSessionCache, cache.set, cache.get, cache.keys, et cache.delete) peuvent également être asynchrones et rendre des Promises rendant ces mêmes objets, les valeurs de retour à null peuvent également être undefined, et les valeur de retour à void peuvent également être une référence à l'objet de cache.

TIP

Vous n'avez pas, dans cette implémentation, à vous occuper de la gestion de la durée de validité du cache, ou de son nettoyage. Ceci est géré en interne par le SDK.

Si vous implémentez un stockage persistant, il est conseillé de chiffrer les données stockées, pour éviter de garder sur disque des clés de chiffrement en clair. Afin de faciliter ceci, la fonction createEncryptionSessionCache reçoit en argument une clé de chiffrement instanciée dbKey, provenant de la databaseKey utilisée pour instanciée le SDK. Aussi, il est indispensable de séparer le cache d'utilisateurs différents éventuels sur la même machine.

TIP

Si vous utilisez un cache chiffré, vous pouvez ne chiffrer que la sessionSymKey reçue, et non la serializationDate, afin de pouvoir rendre la serializationDate sans avoir besoin d'effectuer d'opération de déchiffrement lors d'un appel à cache.get avec l'argument dateOnly à true. Ceci permettra d'optimiser les opérations de nettoyage du cache.

Vous trouverez ci-dessous, à titre d'exemple, le code de createLocalStorageEncryptionSessionCache, qui est une implémentation complète d'un cache personnalisé qui stocke les données du cache de façon chiffrée dans le localStorage du navigateur.

js
const createLocalStorageEncryptionSessionCache = ({ appId, databasePath, dbKey }) => {
  if (!databasePath || !dbKey) throw new Error('Cannot use LocalStorage encryption session cache without a persistent database')

  const prefix = `seald-cache-${appId}-${databasePath}:`

  return {
    async set (id, { sessionSymKey, serializationDate }) {
      try {
        const symKeyBuff = Buffer.from(sessionSymKey, 'base64')
        const encryptedSymKey = await dbKey.encryptAsync(symKeyBuff)
        const obj = { serializationDate, encryptedSymKey: encryptedSymKey.toString('base64') }
        localStorage.setItem(`${prefix}${id}`, JSON.stringify(obj))
      } catch (err) {}
    },
    async get (id, dateOnly = false) {
      try {
        const stored = localStorage.getItem(`${prefix}${id}`)
        if (!stored) return null
        const obj = JSON.parse(stored)
        if (dateOnly) {
          return { serializationDate: obj.serializationDate, sessionSymKey: null }
        } else {
          const encryptedSymKey = Buffer.from(obj.encryptedSymKey, 'base64')
          const symKeyBuff = await dbKey.decryptAsync(encryptedSymKey)
          return { serializationDate: obj.serializationDate, sessionSymKey: symKeyBuff.toString('base64') }
        }
      } catch (err) {
        return null
      }
    },
    async keys () {
      const keys = []
      for (let i = 0; i < localStorage.length; i++) {
        const k = localStorage.key(i)
        if (k.startsWith(prefix)) {
          keys.push(k.substring(prefix.length))
        }
      }
      return keys
    },
    async delete (id) {
      localStorage.removeItem(`${prefix}${id}`)
    }
  }
}

const seald = SealdSDK({
  appId,
  apiURL,
  encryptionSessionCacheTTL: 30 * 60 * 1000,
  createEncryptionSessionCache: createLocalStorageEncryptionSessionCache
})