Skip to content

Encryption Sessions

When you intend to encrypt many messages in a row, for example in an instant chat, you can use an Encryption Session. An Encryption Session uses the same key to encrypt all messages in the session.

The advantages are that your integration code can be simpler, and that it decreases the number of network requests related to retrieving decryption keys.

The downside is that you lose the granularity offered by encrypting each message independently: you cannot add / revoke a recipient on a single message of the session, but only on the session as a whole.

Creating an encryption session

You can create a session with the function sealdSDK.createEncryptionSession.

Upon creation, two elements must be defined:

  • sealdIds : an array of Seald IDs of the users in your application that will be allowed for this encryption session;
  • metadata : an optional argument declaring a non-encrypted metadata to the Seald API. It can be used in the administration of a Seald account.
javascript
const session = await seald.createEncryptionSession(
  {
    sealdIds: [mySealdId, user2SealdId] // recipients: users authorized to decrypt the session messages
  },
  {
    metadata: 'My demo session'
  }
)
const session = await seald.createEncryptionSession(
  {
    sealdIds: [mySealdId, user2SealdId] // recipients: users authorized to decrypt the session messages
  },
  {
    metadata: 'My demo session'
  }
)

The session created is of type EncryptionSession. It has a sessionId property, and several methods.

The user who creates a session is the administrator: only this user will be able to revoke the session.

TIP

If you are creating a session for a group of which the user is a member, you can use the optional argumentencryptForSelf: false so that you do not encrypt directly for their own identities. The user will still be able to decrypt the data thanks to their group membership. This can allow you to improve the performance of the encryption.

Encrypting messages

With the EncryptionSession object, we can then encrypt messages, which will be readable by all recipients of the session.

javascript
const encryptedMessage = await session.encryptMessage('Super secret message') // encrypt a message using the session
const encryptedMessage = await session.encryptMessage('Super secret message') // encrypt a message using the session

This will result in a string of the form:

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

This contains the sessionId which is the identifier of the encryption session and data which is the encrypted data.

You can also use the raw option to only get the raw encrypted message, without the sessionId:

javascript
const rawEncryptedMessage = await session.encryptMessage('Super secret message', { raw: true }) // encrypt a message in raw format
const rawEncryptedMessage = await session.encryptMessage('Super secret message', { raw: true }) // encrypt a message in raw format

This will result in a string of the form:

javascript
'8RwaOppCD3uIJVFv2LoP3XGXpomj0xsMv4qmMVy30Vdqor2w0+DVCXu3j13PEyN2KfJm6SiSrWDRMDziiiOUjQ=='
'8RwaOppCD3uIJVFv2LoP3XGXpomj0xsMv4qmMVy30Vdqor2w0+DVCXu3j13PEyN2KfJm6SiSrWDRMDziiiOUjQ=='

WARNING

A raw encrypted message does not allow you to retrieve the corresponding session, so it is your responsibility to save the sessionId separately in order to be able to retrieve the session.

Retrieving a session

Once a session has been created and a message encrypted, it must be possible to retrieve the session afterwards, including by the sender himself.

For this, there are two possibilities:

  • from the sessionId, but this requires storing it in a separate field in the application;
  • from an encrypted message (except in raw format) with this session directly, which has the advantage of being simpler.

TIP

You can get the sessionId from the session object with its session.sessionId property.

In both cases, we use the sealdSDK.retrieveEncryptionSession method, either with the argument named sessionId if we know it, or with the argument named encryptedMessage:

javascript
const sessionFromId = await seald.retrieveEncryptionSession({ sessionId: sessionId }) // retrieve a session with the sessionId

const sessionFromMessage = await seald.retrieveEncryptionSession({ encryptedMessage: encryptedMessage }) // retrieve a session with an encrypted message in the session
const sessionFromId = await seald.retrieveEncryptionSession({ sessionId: sessionId }) // retrieve a session with the sessionId

const sessionFromMessage = await seald.retrieveEncryptionSession({ encryptedMessage: encryptedMessage }) // retrieve a session with an encrypted message in the session

Decrypting messages

Once the session is retrieved, we can use it to decrypt a message:

javascript
const decryptedMessage = await session.decryptMessage(encryptedMessage) // decrypt a message using the session
const decryptedMessage = await session.decryptMessage(encryptedMessage) // decrypt a message using the session

Add / revoke session recipients

Any recipient of the session who has the 'forward' right (which is the case by default) can add other recipients to it, using the method session.addRecipients.

javascript
await session.addRecipients({ sealdIds: [user3SealdId] }) // requires `'forward'` right
await session.addRecipients({ sealdIds: [user3SealdId] }) // requires `'forward'` right

To revoke a recipient of the session with the session.revokeRecipients method, the user must have the 'revoke' right. By default, only the creator of the session has this right.

Let's imagine a case where user1 added user2 into the session, and user2 added user3.

  • user3 cannot revoke anyone
  • user2 cannot revoke anyone
  • user1 can revoke anyone
javascript
await session.revokeRecipients({ sealdIds: [user3SealdId] }) // requires `'revoke'` right
await session.revokeRecipients({ sealdIds: [user3SealdId] }) // requires `'revoke'` right

A user who has the 'revoke' right (by default, only the creator of the session) can also use the session.revoke method to revoke the entire session and all its content.

javascript
await session.revoke() // requires `'revoke'` right
await session.revoke() // requires `'revoke'` right

Encrypt & decrypt files

An EncryptionSession also exposes two methods encryptFile and decryptFile for using this encryption session to encrypt files.

As explained in the dedicated file encryption guide, these methods support different types (string, Buffer, Blob in the browser only, ReadableStream of Web Streams, and Readable of Node.JS Streams).

Difference with SDK#encryptFile and SDK#decryptFile

By using an EncryptionSession to encrypt/decrypt files, one can reuse the session encryption key on different files/messages, and one manages the recipients at the encryption session level.

Thus, the encryptFile method of an EncryptionSession does not take the recipients argument, and only has the following arguments:

  • clearFile: same as SDK#encryptFile;
  • filename: in this case it is not used as the metadata, but only as the file name in the encrypted tar (see the format of an encrypted file);
  • options: the options object of which only the fileSize property is used, and is only necessary when clearFile is of type Readable or ReadableStream.

Encryption

Here is an example of encryption (with the Buffer type):

js
const session = await seald.createEncryptionSession(
  {
    sealdIds: [mySealdId, user2SealdId] // recipients: users authorized to decrypt the messages of the session
  },
  {
    metadata: 'My demo session'
  }
)

const encryptedBuffer = await session.encryptFile(
  Buffer.from('Secret file content', 'utf8'),
  'SecretFile.txt'
)
const session = await seald.createEncryptionSession(
  {
    sealdIds: [mySealdId, user2SealdId] // recipients: users authorized to decrypt the messages of the session
  },
  {
    metadata: 'My demo session'
  }
)

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

Decryption

To decrypt a file encrypted with a session, we must use the same EncryptionSession (previously retrieved with 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'
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'

One can also directly use SDK#decryptFile which will automatically retrieve the associated EncryptionSession (the EncryptionSession ID is always included in the result of a file encryption):

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'
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

Using the decryptFile method of an EncryptionSession on file encrypted with another session will not work.

TIP

For examples with other types, refer to the examples' section of the file encryption guide. You will just have to take into account the above-mentioned differences.

Caching of encryption sessions

Enabling Caching

To minimize network calls during decryption, you can cache encryption sessions.

In order to enable caching, you need to set a value to the encryptionSessionCacheTTL argument when initializing the SDK. This argument defines the duration, in milliseconds, during which this cache is kept valid (with the value -1 for no cache expiry). By default encryptionSessionCacheTTL is set at 0, which doesn't save the encryptions sessions in cache at all.

js
const seald = SealdSDK({
  appId,
  apiURL,
  encryptionSessionCacheTTL: 30 * 60 * 1000 // keep the cache valid during 30 min
})
const seald = SealdSDK({
  appId,
  apiURL,
  encryptionSessionCacheTTL: 30 * 60 * 1000 // keep the cache valid during 30 min
})

This cache will be automatically populated when creating or retrieving an encryption session, and will be automatically used when retrieving an encryption session. You can, when calling these functions, decide to change this behavior with the useCache: false argument.

js
// will retrieve the session from the cache, without a network call (if the
// cache was enabled at SDK initialization with a non-zero
// `encryptionSessionCacheTTL`, and if this `sessionId` is already in the cache)
const sessionWithCache = seald.retrieveEncryptionSession({ sessionId })

// will retrieve the session by calling the Seald servers
const sessionWithoutCache = seald.retrieveEncryptionSession({ sessionId, useCache: false })
// will retrieve the session from the cache, without a network call (if the
// cache was enabled at SDK initialization with a non-zero
// `encryptionSessionCacheTTL`, and if this `sessionId` is already in the cache)
const sessionWithCache = seald.retrieveEncryptionSession({ sessionId })

// will retrieve the session by calling the Seald servers
const sessionWithoutCache = seald.retrieveEncryptionSession({ sessionId, useCache: false })

Cache cleanup

By default, the cache is cleaned up automatically, with an interval equal to the encryptionSessionCacheTTL, but with a minimum time of 10 seconds. The cleanup removes expired cache entries (older than encryptionSessionCacheTTL) from storage. This means that, in this default configuration and in the worst-case scenario, a cache entry could be stored for up to twice the duration of encryptionSessionCacheTTL.

You can also change this automatic cleanup interval, with the encryptionSessionCacheCleanupInterval argument. This argument defines the interval, in milliseconds, between two automatic cache cleanups (with the value -1 to disable automatic cleanup).

js
const seald = SealdSDK({
  appId,
  apiURL,
  encryptionSessionCacheTTL: 30 * 60 * 1000, // keep the cache valid during 30 min
  encryptionSessionCacheCleanupInterval: 10 * 60 * 1000 // automatically cleanup expired cache entries every 10 min
})
const seald = SealdSDK({
  appId,
  apiURL,
  encryptionSessionCacheTTL: 30 * 60 * 1000, // keep the cache valid during 30 min
  encryptionSessionCacheCleanupInterval: 10 * 60 * 1000 // automatically cleanup expired cache entries every 10 min
})

You can also manually run a cache cleanup, with the sdk.utils.cleanCache function:

js
await sdk.utils.cleanCache()
await sdk.utils.cleanCache()

Customizing the cache storage

By default, this cache is only in memory, specific to each SDK instance. If you want a persistent cache, you can customize the cache storage method, with the argument createEncryptionSessionCache when initializing the SDK.

Cache in the `localStorage

In the browser, the most likely use case is to want to use the localStorage in order to have a persistent cache between page loads and between tabs. The browser version of the SDK contains an implementation of createEncryptionSessionCache which stores data in localStorage, so you don't have to reimplement this common use case yourself. It is called createLocalStorageEncryptionSessionCache, and encrypts the cache entries with the same key as your database before storing them in the localStorage.

This createLocalStorageEncryptionSessionCache is available as a named import of the SDK, in its browser version only. As it stores the cache encrypted with the same keys as your local database, it can only be used with a persistant local database.

js
// In the browser only
import SealdSDK, { createLocalStorageEncryptionSessionCache } from '@seald-io/sdk'

const seald = SealdSDK({
  appId,
  apiURL,
  databasePath,
  databaseKey,
  encryptionSessionCacheTTL: 30 * 60 * 1000, // keep the cache valid during 30 min
  createEncryptionSessionCache: createLocalStorageEncryptionSessionCache
})
// In the browser only
import SealdSDK, { createLocalStorageEncryptionSessionCache } from '@seald-io/sdk'

const seald = SealdSDK({
  appId,
  apiURL,
  databasePath,
  databaseKey,
  encryptionSessionCacheTTL: 30 * 60 * 1000, // keep the cache valid during 30 min
  createEncryptionSessionCache: createLocalStorageEncryptionSessionCache
})

createLocalStorageEncryptionSessionCache is also available as a property of the SealdSDK constructor, in its browser version only.

js
// In the browser only
const seald = SealdSDK({
  appId,
  apiURL,
  encryptionSessionCacheTTL: 30 * 60 * 1000, // keep the cache valid during 30 min
  createEncryptionSessionCache: SealdSDK.createLocalStorageEncryptionSessionCache
})
// In the browser only
const seald = SealdSDK({
  appId,
  apiURL,
  encryptionSessionCacheTTL: 30 * 60 * 1000, // keep the cache valid during 30 min
  createEncryptionSessionCache: SealdSDK.createLocalStorageEncryptionSessionCache
})

Manual cache

You can also make your own implementation of createEncryptionSessionCache.

This argument takes a function, which receives several arguments that may help you in your implementation. It must return an object that will serve as a cache, EncryptionSessionCache.

This object must have four methods:

  • set (id: string, value: { sessionSymKey: string, serializationDate: number }): void: takes an id and an object, and must store it.
  • get (id: string, dateOnly?: boolean): { sessionSymKey: string|null, serializationDate: number }: takes an id and must return any object previously stored with that id, or null. If the dateOnly argument is set to true, this function may only return serializationDate in the returned object, with sessionSymKey set to null.
  • keys(): Array<string>: must return the list of ids of currently stored entries.
  • delete(id: string): void: must delete the cache entry corresponding to the passed id.

All of these functions (createEncryptionSessionCache, cache.set, cache.get, cache.keys, and cache.delete) can also be asynchronous and return Promises returning these same objects, the null return values can also be undefined, and the void return values can also be a reference to the cache object.

TIP

You do not have to worry, in this implementation, about managing the cache validity time, or cleaning it up. This is handled internally by the SDK.

If you are implementing persistent storage, it is advisable to encrypt the stored data, to avoid keeping encryption keys on disk in cleartext. To facilitate this, the createEncryptionSessionCache function receives as argument an instantiated dbKey encryption key, derived from the databaseKey used to instantiate the SDK. Also, it is essential to separate the cache from possible different users on the same machine.

TIP

If you are using an encrypted cache, you can encrypt only the received sessionSymKey, not the serializationDate, so that you can return the serializationDate without needing to perform a decryption operation when calling cache.get with the dateOnly argument set to true. This will optimize cache cleanup operations.

Below, as an example, is the code for createLocalStorageEncryptionSessionCache, which is a full implementation of a custom cache that stores cache data encrypted in the browser's localStorage.

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
})
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
})