Skip to content

Identity management

After importing and instantiating the SDK, it will need to have an "identity" to perform other operations.

A user is represented by several "identities", usually one per device. These identities are created through the SDK. They can either be kept only in volatile memory, or in a persistent local database. They can also be saved onto our servers in a secure way to be able to reuse them later.

To save an identity on our servers securely, there are two ways to protect it:

For a manual management of the protection of an identity that would not be covered by these plugins, do not hesitate to contact us or use the manual export.

uml diagram

Creating identities

A Seald user is represented by several "identities". These are actually private keys generated locally on the user's device with the SDK.

They are all linked together by a signature chain. For more details about our technology, please visit the dedicated page.

We distinguish two cases:

  • a first identity ;
  • the next identities that are attached to it, called "sub-identities".

First identity

To create an identity for a new user, use the sdk.initiateIdentity function.

This function requires a JSON Web Token. A dedicated guide will explain how to generate these license JWTs.

js
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKSPassword(keyStorageURL)] })
await seald.initialize()
// Create their Seald identity
await seald.initiateIdentity({ signupJWT: 'signup-jwt-value' })

Personalized identifier

It is possible to add a custom identifier, called connector, in string format, via the use of a JWT. This can be done when creating an identity, or later using the sdk.pushJwt function.

The connector must be in the format ${IDENTIFIER}@${APP_ID}. The connector type must always be 'AP'.

An identity can have multiple identifiers. Adding a custom ID is done via the connector_add key defined in this documentation.

TIP

The old name of this feature, now deprecated, used to be userId. The userId used to correspond to the IDENTIFIER part of the connector format described above, without the @${APP_ID}.

Sub-identities

From an existing identity of a user you can create a "sub-identity" using the function sdk.createSubIdentity on an SDK instance which has already been initialized.

This sub-identity will then be able to decrypt everything that the initial identity can decrypt: when another user encrypts something for that user, both the identity and the subidentity will be able to decrypt it.

This is typically used to have a separate sub-identity per device. For example, when the user logs in to a new device, we can retrieve a "main" identity protected by password or with 2-man rule, then create a new sub-identity linked to it for this device. It is then possible to store this sub-identity in localStorage, and to retrieve it when opening a new page, in order to have persistent sessions accross tabs.

js
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKSPassword(keyStorageURL)] })
await seald.initialize()
// Create its Seald identity
await seald.initiateIdentity({ signupJWT: 'signup-jwt-value' })

// Create a sub-identity
const { subIdentity } = await seald.createSubIdentity()

This function returns a Buffer similar to the one to the one created by sdk.exportIdentity and described in the manual export paragraph.

TIP

To accelerate the creation of sub-identities, you can pre-generate the private keys by calling the seald.preGenerateIdentityKeys() function in advance.

Examples
js
// Pre-generate 1 key
sdk.preGenerateIdentityKeys(1)

/*
Do other stuff, to let time for the pre-generation to finish as background task
*/

// Create a sub-identity faster
const { identity } = await seald.createSubIdentity()

You can then import this Buffer into another instance of the SDK with the function sdk.importIdentity, or save it directly on SSKS with the functions sdk.ssks2MR.saveIdentity or sdk.ssksPassword.saveIdentity.

WARNING

Having a very large number of sub-identities for the same user (several dozen) can slow down operations concerning this user, whether it is the creation of a new sub-identity or even simply the creation of an encryption session for them.

In case a user has a very large number of devices, or re-logs in very regularly, we advise you not to create a separate sub-identity for each connection, but simply use the identity stored on SSKS directly.

Persistent local database

How it works

By default, the Seald SDK only saves its database in volatile memory: if you create an identity, then close and re-open your browser tab, the created identity will not be accessible anymore and will be lost, except if you have protected it onto SSKS with a password or with two-man-rule. It this case, even if the user has an authenticated session, they will have to re-type their password, or re-authenticate with their email, in order to retrieve their Seald identity.

In order to have a better user experience, we can use a persistent local database, automatically stored securely in the browser by the Seald SDK, encrypted with a key stored on your backend called databaseKey.

TIP

The Seald database uses nedb, which in the browser does not necessarily use the localStorage, but choses (through localForage) the best storage method between IndexedDB, WebSQL or localStorage depending on your browser.

uml diagram

When the user re-opens the application, the databaseKey is retrieved from the backend through an authenticated session, and this key will be used to decrypt the encryptedDB.

uml diagram

Using a persistent local database

To use a persistent local database, you just need to add a databasePath argument to save the database, and a databaseKey argument to encrypt it, when initializing the SDK.

The server can either use a single databaseKey per user for all their devices, or it can use a different databaseKey per session. Using a different databaseKey per session is a bit more secure, but implies a more complicated data model. We will assume in the following a different databaseKey per session.

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

Here, databaseKey and sessionID must be generated and returned by the server.

We add sessionID in the databasePath in order for it to be unique for each session.

Indeed, if the same user logs-out, then logs back in, the server will have generated a new databaseKey. We therefore cannot use the same database: the database must be unique for each session, so we make databasePath change depending on the sessionID.

TIP

You can also replace databaseKey with databaseRawKey.

Note that databaseRawKey must be the Base64 string encoding of a cryptographically random buffer of 64 bytes.

Technically, this avoids deriving the databaseKey with scrypt, which improves the speed of instantiation.

If in doubt, use databaseKey instead.

Examples

Using a databaseRawKey:

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

Generating a suitable databaseRawKey:

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

const rawKeyBuffer = await randomBytes(64)
// This format is strict : it must be exactly 64 bytes, encoded as Base64
// and must be from a cryptographically-safe random source
const rawKey = rawKeyBuffer.toString('base64')

WARNING

If databasePath is defined, but databaseKey (or databaseRawKey) is not, the SDK will use a fixed encryption key for the database (derived from the appId), which is a bad practice and could represent a minor security risk.

Unlike the protection of a user's identity on SSKS, here the usage is the same whether you want to create a new identity and store it in a persistent database, or retrieve an existing identity from it.

You will find more details, especially on what you will have to implement on the backend side, on the example project's page about the persistent database.

Password protection

Password protection of an identity is based on a derivation of a password known by the user into a symmetric key that is used to encrypt and decrypt the identity.

WARNING

A "forgotten password" functionality cannot be implemented with this method without loss of data. If it is needed in your application, use the protection with 2-man-rule instead.

How it works

This encrypted identity is then stored on a dedicated service named Seald SDK Key Storage or SSKS.

The password protection of an identity works as follows:

uml diagram

The retrieval of a password protected identity works as follows:

uml diagram

The @seald-io/sdk-plugin-ssks-password plugin allows to do this automatically.

TIP

More precisely, the user's password is derived twice :

  • once by combining it with the userId and the appId to give a "storage key" which is not secret ;
  • a second time by combining it with a random "salt", the userId and the appId to give an "encryption key" which is secret.

The identity is encrypted with the "encryption key". Then, are sent to SSKS:

  • the "storage key" ;
  • the "salt" concatenated with the encrypted identity.

The identity is retrieved by sending to SSKS the storage key which returns the salt concatenated with the encrypted identity. The salt is then used to obtain the encryption key which is then used to decrypt the identity.

Save the identity

To use this storage method, you need to add the @seald-io/sdk-plugin-ssks-password plugin when instantiating the SDK, then after creating the identity, store it on SSKS using the sdk.ssksPassword.saveIdentity function.

javascript
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKSPassword(keyStorageURL)] })
await seald.initialize()
// Create the Seald identity
await seald.initiateIdentity({ signupJWT: 'signup-jwt-value' })
// Save the password-encrypted Seald identity on the SSKS server
await seald.ssksPassword.saveIdentity({
  userId: 'myUserId',
  password: 'user-known-secret-password'
})

The identity is now saved.

TIP

You can also store another sub-identity, instead of the identity of the current SDK instance:

javascript
// Create a sub-identity
const { identity: mySubIdentity } = await seald.createSubIdentity()
// Save the new Seald identity, encrypted by the password, on the SSKS server
await seald.ssksPassword.saveIdentity({
  userId: 'myUserId',
  password: 'user-known-secret-password',
  identity: mySubIdentity
})

Retrieve the identity

To retrieve the identity at login, call the sdk.ssksPassword.retrieveIdentity function, with the same userId and password arguments as used in the save-identity.

TIP

If you use an incorrect password multiple times, the server may throttle your requests. In this case, you will receive an error Request throttled, retry after {N}s, with {N} the number of seconds during which you cannot try again.

javascript
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKSPassword(keyStorageURL)] })
await seald.initialize()
// Retrieve the password-encrypted Seald identity from the SSKS server
await seald.ssksPassword.retrieveIdentity({
  userId: 'myUserId',
  password: 'user-known-secret-password'
})

The SDK instance is now ready to be used!

WARNING

It is recommended not to retrieve the same identity with ssksPassword.retrieveIdentity on multiple devices at the same time, at the same exact instant, for example during automated tests. Please wait until one of the devices has finished retrieving the identity before starting the retrieval on another device.

Change the password

When the user changes the password, you can use the sdk.ssksPassword.changeIdentityPassword function to have the identity stored with the new password and delete the one stored with the old one.

javascript
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKSPassword(keyStorageURL)] })
await seald.initialize()
// Retrieve the Seald identity, encrypted with the old password, from the SSKS server
await seald.ssksPassword.retrieveIdentity({
  userId: 'myUserId',
  password: 'user-known-secret-password' // old password
})
// change the password
await seald.ssksPassword.changeIdentityPassword({
  userId: 'myUserId',
  currentPassword: 'user-known-secret-password', // old password
  newPassword: 'new-user-known-secret-password' // new password
})

The password is now changed and only the new one can be used.

TIP

If the old password is not known (typically a forgotten password), this plugin does not allow to recover the identity. If the "forgotten password" functionality is needed in your application, use the 2-man-rule protection instead.

WARNING

This operation does not change the keys of the identity itself, it only re-protects it with a new password. If the old password is considered breached, you must also create a new identity and revoke the old one.

Password also used for authentication

In case you want to use the same password for identity protection as for authentication, it cannot be transmitted to the server as is, otherwise the server could then reconstruct the user's key, thus breaking the end-to-end encryption.

In this case, the login and account creation steps must be modified to introduce password pre-derivation using secure derivation functions so that your server never has access to the raw password, but only to a derived version.

You can find an example implementation here.

TIP

In case you can't change the authentication function (e.g. in case of federated authentication), then you should use a second password or another identity protection mode like 2-man rule.

Customize the password derivation

As explained previously, the password provided by the user is derived into a "storage key", and an "encryption key". This derivation is done with scrypt.

For advanced use however, you can bypass this mechanism, and directly pass raw "storage key" and "encryption key" as arguments.

The storage key does not have to be secret from your server. It must however be unique, and secret from any other user, as it would allow the saved identity to be deleted. It must be a string, maximum 256 characters long, and composed of the following allowed characters: /A-Za-z0-9+\/=\-_@./.

For this, several strategies are possible:

  • you can use a random string generated by the server, and simply pass it to the front-end
  • you can also derive it from the password, potentially with another salt stored on the server

The encryption key, however, can only be known by the user in their frontend and must absolutely remain secret from any other actor, including your server. It must be the Base64 string encoding of a 64 bytes buffer. The usual way is to derive it from the password, if possible with a unique random salt generated and stored by your server.

Example of custom derivation of rawEncryptionKey with 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,
  // `rawEncryptionKey` format is strict :
  // it must be exactly 64 bytes, encoded as Base64
  rawEncryptionKey: derivedKey,

  // `rawStorageKey` is more lenient : it must be unique and secret,
  // but can be anything up to 256 characters long
  // Allowed characters : /A-Za-z0-9+\/=\-_@./
  rawStorageKey: serverStoredRawStorageKey
})

Protection with 2-man-rule

The 2-man-rule protection mode consists of "splitting" the identity to be protected in two and placing one half on the application's back-end subject to authentication, and the other half on Seald SDK Key Storage (SSKS) subject to independent authentication.

The goal is that a user has "the right" to lose all their passwords and all their devices without losing any data. The trade-off is that this mode is not strictly "end-to-end".

It is necessary to implement on the server side the storage of a secret stored and delivered against authentication, as well as an interaction with SSKS.

How it works

More specifically, to protect an identity, the protocol is as follows (in the normal case):

uml diagram

TIP

In the case where an identity has previously been stored onto SSKS with the same email address, the SSKS server will then demand an authentication with an email challenge, similarly to what is described in the following section about retrieving an identity.

For more details, you may refer to the guide about Integrating 2-man-rule on your backend.

To retrieve an identity, the protocol is as follows:

uml diagram

Thus, if the backend is breached, the encryptedIdentity stored on SSKS will not be breached, and the backend cannot simulate an authentication to SSKS.

Conversely, if SSKS is breached, the twoManRuleKey will not be breached and SSKS cannot simulate authentication to the backend.

WARNING

This mode is not strictly end-to-end in the sense that the key to decrypt a user's data can be retrieved without the user's consent by combining twoManRuleKey on SSKS and encryptedIdentity on the backend.

TIP

Seald takes the following precautions to ensure the privacy of data stored on SSKS :

  • use of servers not subject to the Cloud Act (using OVH in France) so that the 2-man rule protection mode is robust when using host providers subject to the extra-territoriality of US law for the backend;
  • over-encryption at rest of sensitive fields (encryptedIdentity) ;
  • hashing and salting of the users' e-mail addresses to which the encryptedIdentity are associated, so that they are only known at runtime. Thus, a breach at rest of SSKS won't reveal to which e-mail address an encryptedIdentity is associated.

WARNING

Email addresses and phone numbers sent to the SSKS server must be normalized.

Protection of an identity

To use this method of protection, you must install, import & add when instantiating the SDK the @seald-io/sdk-plugin-ssks-2mr plugin.

Your application server must then :

  • create a new user on SSKS by giving its email address ;
  • generate an SSKS session for this user;
  • generate a secret twoManRuleKey for this user (a sufficiently robust random string of characters, for example a UUID would be a good choice)
  • transmit the sessionId and the secret twoManRuleKey to the front-end which we will symbolize by API.getSSKSSession() in the example.

The user can then store their identity using the sdk.ssks2MR.saveIdentity function.

You can find the SSKS API documentation for your application server here.

javascript
// Instantiate the SDK
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKS2MR(keyStorageURL)] })
await seald.initialize()
// We create a Seald identity as seen in the first part
await seald.initiateIdentity({ signupJWT: 'signup-jwt-value' })

// We make an API call to the application server to get the `sessionId` and the `twoManRuleKey`.
const { sessionId, twoManRuleKey } = APIClient.getSSKSSession() // API endpoint to develop on your end

await seald.ssks2MR.saveIdentity({
  userId: 'myUserId',
  sessionId,
  // the user's email address must be repeated to avoid a MITM by your application server
  authFactor: {
    type: 'EM',
    value: 'user@domain.com'
  },
  twoManRuleKey // `twoManRuleKey` is the key stored by your application server to secure this user's identity
})

The identity is now saved.

TIP

You can also store another sub-identity, instead of the identity of the current SDK instance:

javascript
// Create a sub-identity
const { identity: mySubIdentity } = await seald.createSubIdentity()
// Save the new Seald identity, encrypted by `twoManRuleKey`, on the SSKS server
await seald.ssks2MR.saveIdentity({
  userId: 'myUserId',
  sessionId,
  authFactor: {
    type: 'EM',
    value: 'user@domain.com'
  },
  twoManRuleKey,
  identity: mySubIdentity
})

TIP

The SSKS server cannot access the user's identity, because it does not have access to the secret key twoManRuleKey kept by your server. Conversely, your application server cannot access it either, because to retrieve an identity it would need a challenge sent by email or SMS, which it nerver receives and cannot forge.

Retrieving an identity

Then, upon a new instantiation of the SDK, your backend must generate in the same way a session for the user, and pass the sessionId and the twoManRuleKey to the front-end. The SSKS server then sends an email containing a challenge (valid for 6h) to the user's email address.

TIP

In a test environment, your server can use the fake_otp: true argument during session generation, so that the challenge is not actually sent. It will then be set to 'aaaaaaaa'.

For more details, see the dedicated paragraph in the guide about the 2-man-rule.

Then you need to call the function sdk.ssks2MR.retrieveIdentity with the arguments userId, sessionId, email, challenge and twoManRuleKey.

javascript
// We instantiate the SDK
const seald = SealdSDK({ appId, apiURL, plugins: [SealdSDKPluginSSKS2MR(keyStorageURL)] })
await seald.initialize()

// We make an API call to the application server to get the `sessionId` and the `twoManRuleKey`.
const { sessionId, twoManRuleKey } = APIClient.getSSKSSession() // API endpoint to develop on your end

/* an email is sent to the user containing a `challenge` valid for 6h */
/* ... */
/* the `challenge` is retrieved by the user and given in the page context */

await seald.ssks2MR.retrieveIdentity({
  userId: 'myUserId',
  sessionId,
  // the user's email address must be repeated to avoid a MITM by your application server
  authFactor: {
    type: 'EM',
    value: 'user@domain.com'
  },
  challenge: 'challenge sent by email',
  twoManRuleKey // `twoManRuleKey` is the key stored by your application server to secure this user's identity
})

Your SDK instance is now ready to be used!

WARNING

It is recommended not to retrieve the same identity with ssks2MR.retrieveIdentity on multiple devices at the same time, at the same exact instant, for example during automated tests. Please wait until one of the devices has finished retrieving the identity before starting the retrieval on another device.

Changing a user's authFactor

In the user's lifecycle in your application, they may sometimes need to change their email address or phone number.

If this authentication factor is used to store their Seald identity onto SSKS, you will also need to re-register their identity on this new email address or phone number.

To do this, there is no specific API point in the SSKS server API or specific function in the SSKS plugin of the SDK. Simply store the user's identity on the new factor (if the identity is not present locally, you will need to retrieve it on the old authentication factor first), and then use the server API to delete the identity stored on the old factor.

WARNING

To minimize the risk of losing the user's identity, we recommend storing the identity on the new authFactor before deleting it from the old one.

Using a raw TwoManRule Key

When calling ssks2MR.saveIdentity or ssks2MR.retrieveIdentity, you can also replace twoManRuleKey with rawTwoManRuleKey.

Note that rawTwoManRuleKey must be the Base64 string encoding of a cryptographically random buffer of 64 bytes.

Technically, this avoids deriving the twoManRuleKey with scrypt, which improves the speed of execution.

If in doubt, use twoManRuleKey instead.

Using a rawTwoManRuleKey:

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

// Using a `rawTwoManRuleKey` with `ssks2MR.saveIdentity`
await sdk.ssks2MR.saveIdentity({
  userId,
  sessionId,
  authFactor,
  rawTwoManRuleKey: rawKey
})

// Using a `rawTwoManRuleKey` with `ssks2MR.retrieveIdentity`
await sdk.ssks2MR.retrieveIdentity({
  userId,
  sessionId,
  authFactor,
  challenge,
  rawTwoManRuleKey: rawKey
})

Generating a suitable rawTwoManRuleKey:

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

const rawKeyBuffer = await randomBytes(64)
// This format is strict : it must be exactly 64 bytes, encoded as Base64
// and must be from a cryptographically-safe random source
const rawKey = rawKeyBuffer.toString('base64')

Manual export

It is also possible to manage the storage of an identity manually.

For this, you can use the sdk.exportIdentity and sdk.importIdentity functions.

sdk.exportIdentity allows you to export a Buffer containing the current identity of the SDK instance.

Conversely, sdk.importIdentity imports an identity Buffer in the SDK instance in order to make it functional.

DANGER

It is then up to you to ensure the security of these identity exports. This security is of course paramount to ensure the confidentiality of the encrypted data for this user.