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:
- using a password with the plugin
@seald-io/sdk-plugin-ssks-password
; - using two-man rule with the plugin
@seald-io/sdk-plugin-ssks-2mr
.
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.
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.
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.
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
// 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.
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
.
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.
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
:
// front-end
const seald = SealdSDK({
appId,
apiURL,
databaseRawKey: 'uz0BqYCF6IzefVHd8VzvbOZVp12GTMFD2L+UwCGYiRbKhGymgwG5HMSGfNiDt37h5FaTMKHYaqCcGTtH2ZVCzw=='
databasePath: `seald-guide-session-${sessionID}`
})
Generating a suitable databaseRawKey
:
// 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:
The retrieval of a password protected identity works as follows:
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 theappId
to give a "storage key" which is not secret ; - a second time by combining it with a random "salt", the
userId
and theappId
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.
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:
// 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.
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.
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:
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):
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:
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 anencryptedIdentity
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 secrettwoManRuleKey
to the front-end which we will symbolize byAPI.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.
// 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:
// 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
.
// 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
:
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
:
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.