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.
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.
const encryptedMessage = await session.encryptMessage('Super secret message') // encrypt a message using the session
This will result in a string of the form:
'{"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
:
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:
'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
:
const sessionFromId = await seald.retrieveEncryptionSession({ sessionId }) // retrieve a session with the sessionId
const sessionFromMessage = await seald.retrieveEncryptionSession({ 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:
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
.
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 anyoneuser2
cannot revoke anyoneuser1
can revoke anyone
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.
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 asSDK#encryptFile
;filename
: in this case it is not used as themetadata
, but only as the file name in the encrypted tar (see the format of an encrypted file);options
: the options object of which only thefileSize
property is used, and is only necessary whenclearFile
is of typeReadable
orReadableStream
.
Encryption
Here is an example of encryption (with the Buffer
type):
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
):
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):
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.
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.
// 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).
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:
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.
// 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.
// 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 anid
and an object, and must store it.get (id: string, dateOnly?: boolean): { sessionSymKey: string|null, serializationDate: number }
: takes anid
and must return any object previously stored with thatid
, ornull
. If thedateOnly
argument is set totrue
, this function may only returnserializationDate
in the returned object, withsessionSymKey
set tonull
.keys(): Array<string>
: must return the list ofid
s of currently stored entries.delete(id: string): void
: must delete the cache entry corresponding to the passedid
.
All of these functions (createEncryptionSessionCache
, cache.set
, cache.get
, cache.keys
, and cache.delete
) can also be asynchronous and return Promise
s 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
.
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
})