Crate sentc

source
Expand description

§Sentc sdk

It supports user- and group management as well as key rotation and its build to serve large amount of users without any problems.

§Why a new protocol?

  • focus on groups
  • focus on archive, encrypt once and everyone with access can decrypt it without expensive and complex key exchange
  • serverside encrypted key rotation. Much faster than client side rotation.

§Sentc got two components:

  • the client sdks to encrypt and decrypt
  • the server to handle user auth and groups + key management

§Difference between rust sdk and the other

The other sdk’s like javascript or flutter are designed with datastore in mind. In js it uses the indexeddb in the browser and in flutter the device storage or encrypted storage.

This sdk was designed to use your own storage. This means you need to provide more information for each function than in the other sdk’s. But this gives you the flexibility to use it in your programs without compromises.

There is no init function anymore. You can just use the functions you need.

§Usage

In all doc examples we are using the StdKeys implementation. You can switch it by changing the features and use other implementation or even write your own.

§Create an account and an app

To use the sdk, you need a public and secret token.

The public token will be used in your sdk at the frontend and the secret token should only be used at your backend. You can set what function should be called with which token.

  1. Got to https://api.sentc.com/dashboard/register and create an account. You will be redirected to the account dashboard.
  2. Verify the email. We email you to make sure that your email address belongs to you.
  3. In your dashboard click on the blue button: New App. You will get the app tokens and the first jwt keys.

Now you are ready to use the sdk.

§Install the sdk.

Please choose an implementation of the algorithms. There are StdKeys, FIPS or Rec keys. The impl can not work together.

  • StdKeys (feature = std_keys) are a pure rust implementation of the algorithms. They can be used in the web with wasm and on mobile.
  • FIPS keys (feature = fips_keys) are FIPS approved algorithms used from Openssl Fips. This impl does not support post quantum.
  • Rec keys (feature = rec_keys) or recommended keys are a mix of FIPS keys for the classic algorithms and oqs (for post quantum).

The net feature is necessary for the requests to the backend. The library reqwest is used to do it.

cargo add sentc
use sentc::keys::{StdUser, StdGroup};

async fn example()
{
	//register a user
	let user_id = StdUser::register("base_url".to_string(), "app_token".to_string(), "the-username", "the-password").await.unwrap();

	//login a user, ignoring possible Multi-factor auth
	let user = StdUser::login_forced("base_url".to_string(), "app_token", "username", "password").await.unwrap();

	//create a group
	let group_id = user.create_group().await.unwrap();

	//get a group. first check if there are any data that the user need before decrypting the group keys.
	let (data, res) = user.prepare_get_group("group_id", None).await.unwrap();

	//if no data then just decrypt the group keys
	assert!(matches!(res, GroupFetchResult::Ok));

	let group = user.done_get_group(data, None).unwrap();

	//invite another user to the group. Not here in the example because we only got one user so far
	group.invite_auto(user.get_jwt().unwrap(), "user_id_to_invite", user_public_key, None).await.unwrap();

	//encrypt a string for the group
	let encrypted = group.encrypt_string_sync("hello there!").unwrap();

	//now every user in the group can decrypt the string
	let decrypted = group.decrypt_string_sync(encrypted, None).unwrap();

	//delete a group
	group.delete_group(user.get_jwt().unwrap()).await.unwrap();

	//delete a user
	user.delete("password", None, None).await.unwrap();
}

§Limitations

The protocol is designed for async long-running communication between groups.

  • A group member should be able to decrypt the whole communication even if they joined years after the beginning.
  • Group member should get decrypt all messages even if they were offline for years.

The both requirements make perfect forward secrecy impossible. See more at the Protocol how we solved it.

§Contact

If you want to learn more, just contact me contact@sentclose.com.

§User

Sentc provides secure registration and login capabilities out of the box, but we do not store any additional data about the user. If you require additional information, such as an email address or full name, you can register the user from your own backend.

Users are required for encryption/decryption and group joining. Each user has a public and private key, as well as a sign and verify key. These keys are not available through the API, as they are encrypted using the provided password, which the API does not have access to.

A user account can have multiple devices with different logins, but any device can access the user’s keys.

Using Multi-factor auth with an authentication app is also possible.

§Register

The first registration is also considered the first device registration. Please refer to the “Register a Device” section for more information.

The username/identifier can be anything, such as a name, email address, or random number. The username is only required to log in to the correct device.

use sentc::keys::StdUser;

async fn register()
{
	let user_id = StdUser::register("the-username", "the-password").await.unwrap();
}

The username and password can also be generated to ensure a unique and secure login for each device. The following function will create a random device name and password. However, these values are not stored, so please ensure that they are securely stored on the user’s device.

use sentc::user::generate_register_data;

fn main()
{
	let (username, password) = generate_register_data().unwrap();
}

The registration process will throw an error if the chosen username is already taken. To check if a username is still available, you can use the following function, which will return true if the username is still available:

use sentc::user::net::check_user_name_available;

async fn example()
{
	let available = check_user_name_available("base_url", "app_token", "user_identifier").await.unwrap();
}

§Own backend

If you are using your own backend to store additional user information, you can use the prepare function to prepare the registration data. Then, send the output to our API with a POST request to the following endpoint: https://api.sentc.com/api/v1/register

use sentc::keys::StdUser;

fn example()
{
	let input = StdUser::prepare_register("identifier", "password").unwrap();
}

§Login

To log in, you just need to provide the identifier (i.e., username, email, or random number) and the password that was used during registration. The user will then be logged in to the device associated with the given identifier.

The password is not sent to the API, so we cannot access or retrieve the user’s password. This is accomplished by using a password derivation function in the client instead of on the server.

If the identifier or the password is incorrect, this function will throw an error.

The Login function returns an either the user type or data for the mfa validation process.

If you disabled the Mfa in the app options then you can force login to get just the user object back.

§Login forced

With this method the sdk will just return the user object or throw an exception or error if the user enabled mfa because this must be handled in order to get the user data.

use sentc::keys::StdUser;

async fn example()
{
	let user = StdUser::login_forced("base_url".to_string(), "app_token", "username", "password").await.unwrap();
}

§Login with mfa handling

For rust an enum is returned with either the User data or mfa data.

use sentc::keys::{StdUser, StdUserLoginReturn};

async fn login()
{
	let login_res = StdUser::login("base_url".to_string(), "app_token", "username", "password").await.unwrap();

	//check if the enum is PreLoginOut::Otp, if so call mfa_login with the token from the user auth device
	match login_res {
		StdUserLoginReturn::Direct(user) => {
			//the user
		}
		StdUserLoginReturn::Otp(data) => {
			//handle otp
		}
	}
}

§Login auth token

If the user enabled mfa, you must handle it so that the user can continue the login process.

In the above examples we already used the function that works with the auth app of the user.

use sentc::keys::{StdUser, StdPrepareLoginOtpOutput, StdUserLoginReturn};

async fn login()
{
	let login_res = StdUser::login("base_url".to_string(), "app_token", "username", "password").await.unwrap();

	let user = match login_res {
		StdUserLoginReturn::Direct(user) => {
			user
		}
		StdUserLoginReturn::Otp(data) => {
			//get the token first
			mfa_login("<token-from-mfa-app>".to_string(), data).await
		}
	};
}

//token from the auth app
async fn mfa_login(token: String, login_data: StdPrepareLoginOtpOutput) -> StdUser
{
	StdUser::mfa_login("base_url".to_string(), "app_token", token, "username", login_data).await.unwrap()
}

§Login with recovery key

If the user is not able to create the token (e.g. the device is broken or stolen), then the user can also log in with a recovery key. These keys are obtained after mfa was enabled. If the user uses one key then the key gets deleted and can’t be used again.

use sentc::keys::{StdUser, StdPrepareLoginOtpOutput, StdUserLoginReturn};

async fn login()
{
	let login_res = StdUser::login("base_url".to_string(), "app_token", "username", "password").await.unwrap();

	let user = match login_res {
		StdUserLoginReturn::Direct(user) => {
			user
		}
		StdUserLoginReturn::Otp(data) => {
			//get the token first
			mfa_recovery_login("<recovery-key>".to_string(), data).await
		}
	};
}

//token from the auth app
async fn mfa_recovery_login(recovery_key: String, login_data: StdPrepareLoginOtpOutput) -> StdUser
{
	StdUser::mfa_recovery_login("base_url".to_string(), "app_token", recovery_key, "username", login_data).await.unwrap()
}

§User object

After successfully logging in, you will receive a user object, which is required to perform all user actions, such as creating a group.

You can export the user struct either when owning the struct or from its ref and import it with the parse fn from String:

use sentc::keys::StdUser;

fn example(user: StdUser)
{
	let export = user.to_string().unwrap();

	//don't forget the type
	let imported_user: StdUser = export.parse().unwrap();
}

fn example_ref(user: &StdUser)
{
	let export = user.to_string_ref().unwrap();

	let imported_user: StdUser = export.parse().unwrap();
}

§The User Data

he data contains all information about the user account and the device that sentc needs.

For the device:

  • Asymmetric key pairs only for the device.
  • Device ID.

For user account:

  • Asymmetric key pairs for the account (which are also used to join a group).
  • The actual JWT for this session.
  • The refresh token for this session.
  • User ID

To get the data, just access the data in the user struct.

use sentc::keys::StdUser;

fn example_ref(user: &StdUser)
{
	let refresh_token = user.get_refresh_token();
	let user_id = user.get_user_id();
	let device_id = user.get_device_id();
}

§Authentication and JWT

After logging in, the user receives a JSON Web Token (JWT) to authenticate with the sentc API. This JWT is only valid for 5 minutes. But don’t worry, the SDK will automatically refresh the JWT when the user tries to make a request with an invalid JWT.

To refresh the JWT, a refresh token is needed. This token is obtained during the login process.

There are three strategies to refresh a JWT. However, this is only necessary if you must use HTTP-only cookies for the browser. If you are using other implementations, stick with the default.

If a function returned this error: SentcError::JwtExpired then you have to refresh the jwt to make the request.

use sentc::keys::StdUser;

async fn refresh_jwt(user: &mut StdUser)
{
	let fresh_jwt = user.refresh_jwt().await.unwrap();
}

§Multi-Factor authentication

Sentc uses Time-based one-time password (Totp) for Multi-factor auth. These tokens can easily be generated by any totp generator app like google authenticator, authy or free otp.

A secret is generated alone side with six recovery keys (just in case if the user lost access to the auth device). The user should print out or store the recovery keys to still get access to the account.

The auth app needs the secret and information about the used algorithm. The simplest way is to get an otpauth url and transform it into a qr code, so the auth app can scan it.

The mfa is bind to all devices in the user account not just the actual one.

The user must be logged in, in order to activate mfa and has to enter the password again. issuer and audience are needed for the auth app. Issuer can be your app name and audience the username email or something else.

use sentc::keys::StdUser;

//get the user object after login
async fn example(user: &mut StdUser)
{
	let (url, recover_codes) = user.register_otp("issuer", "audience", "password", None, None).await.unwrap();
}

§Reset mfa

If the user only got one recovery key left or the device with the auth app ist stolen or lost then resetting the mfa is the best practice

The old recovery keys and the old secret will be deleted and replaced by new one. The return values are the same as in the register process.

The user also needs to enter a totp from an auth app or a recovery key in order to reset it. This will make sure that only a person with access can change it.

The last parameter is for the function to know if a recovery key (Some(true)) or a normal top (None) is used.

use sentc::keys::StdUser;

async fn example(user: &StdUser)
{
	let (url, recover_codes) = user.reset_otp("issuer", "audience", "password", Some("token from auth app".to_string()), None).await.unwrap();
}

§Disable mfa

To disable the mfa use this function:

A totp or recovery key is also needed.

use sentc::keys::StdUser;

async fn example(user: &mut StdUser)
{
	user.disable_otp("password", Some("token from auth app".to_string()), None).await.unwrap();
}

§Get totp recovery keys

To get the recovery keys so the user can later store them:

A totp or recovery key is also needed.

use sentc::keys::StdUser;

async fn example(user: &StdUser)
{
	let keys = user.get_otp_recover_keys("password", Some("token from auth app".to_string()), None).await.unwrap();
}

Alternative you can disable the mfa from your backend, e.g. if the user looses the recovery keys and the device access.

§Register Device

To register a new device, the user must be logged in on another device. The process has three parts: preparing the data on the new device, sending the data to the logged-in device, and adding the new device.

To produce the input on the new device, follow these steps. The identifier and password could be generated the same way as during user registration.

use sentc::keys::StdUser;

async fn example()
{
	let server_res = StdUser::register_device_start("base_url".to_string(), "app_token", "device_identifier", "device_password").await.unwrap();
}

This function will also throw an error if the username still exists for your app

Send the Input to the Logged-In Device (possibly through a QR code, which the logged-in device can scan), and call this function with the input.

use sentc::keys::StdUser;

async fn example(user: &StdUser, server_res: String)
{
	user.register_device(server_res).await.unwrap();
}

This will ensure that only the user’s devices have access to the user’s data.

After this, the user can log in on the new device.

§Get devices

The device list can be fetched through pagination.

use sentc::keys::StdUser;

async fn example(user: &StdUser)
{
	let list = user.get_devices(None).await.unwrap();

	//To get more devices use:
	let list = user.get_devices(Some(&list.last().unwrap())).await.unwrap();
}

§Change password

The user must enter the old and new passwords.

use sentc::keys::StdUser;

async fn example(user: &StdUser)
{
	user.change_password("old_password", "new_password", None, None).await.unwrap();
}

This function will also throw an error if the old password was not correct

If the user enabled mfa then you also need to enter the token or a recovery key.

use sentc::keys::StdUser;

async fn example(user: &StdUser)
{
	user.change_password("old_password", "new_password", Some("auth_token_if_any".to_string()), None).await.unwrap();

	//with recovery key
	user.change_password("old_password", "new_password", Some("recovery_key".to_string()), Some(true)).await.unwrap();
}

§Reset password

To reset a password, the user must be logged in on the device. A normal reset without being logged in is not possible without losing access to all data because the user must have access to the device keys. If the user doesn’t have access, they can no longer decrypt the information because the sentc API doesn’t have access to the keys either.

When resetting the password, the secret keys of the device will be encrypted again with the new password.

use sentc::keys::StdUser;

async fn example(user: &StdUser)
{
	user.reset_password("new_password").await.unwrap();
}

§Reset user password with data loss

To reset the user password from your backend call this endpoint.

  • https://api.sentc.com/api/v1/user/forced/reset_user with a put request
  • the data is the same string that the user got from the prepareRegister function.
  • All user devices will be deleted and the user can’t decrypt any of the old data or any of the data inside groups but the user stays in all groups.
  • The user has to be re invited to all groups

§Update user or device identifier

This will change the user identifier. The function will throw an error if the identifier is not available. Only the identifier of the actual device will be changed.

use sentc::keys::StdUser;

async fn example(user: &mut StdUser)
{
	user.update_user("new_user_name".to_string()).await.unwrap();
}

This function will also throw an error if the identifier still exists for your app

§Delete device

To delete a device, a device password from any device and the device ID are needed. The ID can be obtained from the user data or by fetching the device list.

use sentc::keys::StdUser;

async fn example(user: &StdUser, device_id: &str)
{
	user.delete_device("password", device_id, None, None).await.unwrap();
}

This function will also throw an error if the password was not correct

If the user enabled mfa then you also need to enter the token or a recovery key.

use sentc::keys::StdUser;

async fn example(user: &StdUser, device_id: &str)
{
	user.delete_device("password", device_id, Some("auth_token_if_any".to_string()), None).await.unwrap();

	//with recovery key
	user.delete_device("password", device_id, Some("recovery_key".to_string()), Some(true)).await.unwrap();
}

Get the device id from the user data:

use sentc::keys::StdUser;

fn example(user: &StdUser)
{
	let device_id = user.get_device_id();
}

§Delete account

To delete the entire account, use any device password.

use sentc::keys::StdUser;

async fn example(user: &StdUser)
{
	user.delete("password", None, None).await.unwrap();
}

This function will also throw an error if the password was not correct

If the user enabled mfa then you also need to enter the token or a recovery key.

use sentc::keys::StdUser;

async fn example(user: &StdUser)
{
	user.delete("password", Some("auth_token_if_any".to_string()), None).await.unwrap();

	//with recovery key
	user.delete("password", Some("recovery_key".to_string()), Some(true)).await.unwrap();
}

§Public user information

Only the newest public key is used. You can just fetch the newest public key or a verify key by id.

Public key:

use sentc::keys::StdUser;

async fn example(user: &StdUser, user_id: &str)
{
	let public_key = user.get_user_public_key_data(user_id).await.unwrap();
}

Verify Key:

This key can only be fetched by id because to verify data you need a specific verify key.

use sentc::keys::StdUser;
use sentc::crypto_common::user::UserVerifyKeyData;

async fn example(user: &StdUser, user_id: &str, verify_key_id: &str)
{
	let verify_key: UserVerifyKeyData = user.get_user_verify_key_data(user_id, verify_key_id).await.unwrap();
}

§Create safety number

A safety number (or public fingerprint) can be used to check if another user is the real user. Both users can create a safety number with each other and can then check if the number is the same. This check should be done live in person or via video chat.

use sentc::keys::StdUser;
use sentc::crypto_common::user::UserVerifyKeyData;

fn example(user: &StdUser, other_user_id: &str, other_user_key: &UserVerifyKeyData)
{
	let number = user.create_safety_number_sync(Some(other_user_id), Some(other_user_key)).unwrap();
}

The other side:

use sentc::keys::StdUser;
use sentc::crypto_common::user::UserVerifyKeyData;

fn example(user: &StdUser, first_user_id: &str, first_user_key: &UserVerifyKeyData)
{
	let number2 = user.create_safety_number_sync(Some(first_user_id), Some(first_user_key)).unwrap();
}

§Verify a users public key

To make sure that the public key which is used to encrypt the group keys really belongs to the user, this key can be verified. A safety number can be helpful to check if the verify key is the right one.

use sentc::keys::StdUser;

async fn example(user: &StdUser, user_id: &str)
{
	//fetch a public key of a user
	let public_key = user.get_user_public_key_data(user_id).await.unwrap();

	let verify = StdUser::verify_user_public_key("base_url".to_string(), "app_token", user_id, &public_key).await.unwrap();
}

To check the right verify key of this public key the user can get it:

use sentc::keys::StdUser;
use sentc::crypto_common::user::{UserVerifyKeyData, UserPublicKeyData};

async fn example(user: &StdUser, user_id: &str)
{
	//fetch a public key of a user
	let public_key: UserPublicKeyData = user.get_user_public_key_data(user_id).await.unwrap();

	//is an Option
	let verify_key_id = public_key.public_key_sig_key_id.unwrap();

	let verify_key: UserVerifyKeyData = user.get_user_verify_key_data(user_id, verify_key_id).await.unwrap();

	//create a safety number with this key
	let number = user.create_safety_number_sync(Some(user_id), Some(&verify_key)).unwrap();

	let verify = StdUser::verify_user_public_key("base_url".to_string(), "app_token", user_id, &public_key).await.unwrap();
}

§Key rotation

Just like in groups, users can also do a key rotation. New keys are generated and used and the old will only use the decrypt all data.

The rotation can be started from any device and needs to finish on the other devices so that they got the new keys too.

To start it:

async fn start_rotation(user: &mut StdUser)
{
	user.key_rotation().await.unwrap();
}

After the rotation use this function to get the new keys for the other devices:

async fn finish_rotation(user: &mut StdUser)
{
	user.finish_key_rotation().await.unwrap();
}

§Encrypt for a user

When encrypting content for a user, the content is encrypted using the user’s public key. However, it is important to note that public/private key encryption may not be suitable for handling large amounts of data. To address this, best practice is to use a symmetric key to encrypt the content, and then encrypt the symmetric key with the user’s public key (as with groups).

When encrypting content for a user, the reply user ID is required.

We highly recommend creating a group even for one-on-one user communication. This allows the user who encrypts the data to also decrypt it later without any additional configuration. To achieve this, simply auto-invite the other user and use the “stop invite” feature for this group.

For more information on auto-invite functionality, please see the auto invite section.

§Encrypt raw data

Raw data are bytes (&u8).

use sentc::keys::StdUser;

fn example(user: &StdUser, data: &[u8])
{
	let encrypted = user.encrypt_sync(data, user_public_key, false).unwrap();
}

§Decrypt raw data

For user this is a little more complicated. Only the user which user id was used in encrypt can decrypt the content.

Raw data are bytes (&u8).

use sentc::keys::StdUser;

fn example(user: &StdUser, encrypted: &[u8])
{
	let decrypted = user.decrypt_sync(encrypted, None).unwrap();
}

§Encrypt strings

Encrypting strings is a special case, as it requires converting the text to bytes using an UTF-8 reader before encryption.

To simplify this process, Sentc offers string encryption functions that handle this conversion for you.

use sentc::keys::StdUser;

fn example(user: &StdUser, data: &str)
{
	let encrypted = user.encrypt_string_sync(data, user_public_key, false).unwrap();
}

§Decrypt strings

The same as decrypt raw data but this time with a string as encrypted data.

use sentc::keys::StdUser;

fn example(user: &StdUser, encrypted: &str)
{
	let decrypted = user.decrypt_string_sync(encrypted, None).unwrap();
}

§Sign and verify the encrypted data

Sentc offers the ability to sign data after encryption and verify data before decryption. This ensures the authenticity of the encrypted data and protects against potential tampering.

§Sign

For sign, the newest sign key of the user is used.

use sentc::keys::StdUser;

fn example(user: &StdUser, data: &str)
{
	let encrypted = user.encrypt_string_sync(data, user_public_key, true).unwrap();
}

§Verify

For verify, the right verify key needs to be fetched first.

use sentc::keys::StdUser;

fn example(user: &StdUser, encrypted: &str)
{
	let decrypted = user.decrypt_string_sync(encrypted, Some(user_verify_key)).unwrap();
}

§Group

Everything in a group can be shared with every group member. Every group member gets access to the keys of the group. If you encrypt something for a group, every group member is able to decrypt it. It can also be used for 1:1 user sessions for more flexibility.

In sentc everything is a group, even the user account with all devices as members.

A group has a public/private key pair and symmetric key. All of those keys are coupled together via an internal ID. With a key rotation, new keys are created, but the old one can still be used. No extra key management is needed on your side.

§Create a group

When creating a group, all group private keys are encrypted in the client by the creator’s public key and sent to the server.

Call create_group() from the User object after logging in a user.

use sentc::keys::StdUser;

async fn example(user: &StdUser)
{
	let group_id = user.create_group().await.unwrap();
}

When you use your own backend, call the prepare function. This function returns the client data for a new group. Make a POST request to our API (https://api.sentc.com/api/v1/group) with this data from your backend. Don’t forget to include the Authorization header with the JWT.

use sentc::keys::StdUser;

fn example(user: &StdUser)
{
	let input = user.prepare_create_group().unwrap();
}

§Fetch a group

To access the keys of a group, a user can fetch them from the API and decrypt them for their own use. To fetch a group, use the group ID as a parameter. This returns a group object that can be used for all group-related actions.

In the rust version there are two different functions to call. Data are the group data to decrypt and res will signal if you need to fetch more keys for the user. This can happen if the device of the user missed a key rotation and the group invite was done by the new keys of the user. In this case, just finish the key rotation on this device.

The 2nd function will then decrypt the group keys when the user got all keys.

use sentc::keys::StdUser;
use sentc::group::net::GroupFetchResult;

async fn example(user: &StdUser)
{
	let (data, res) = user.prepare_get_group("group_id", None).await.unwrap();

	assert!(matches!(res, GroupFetchResult::Ok));

	let group = user.done_get_group(data, None).unwrap();
}

§Get all groups

To retrieve all group IDs where the user is a member, use this function:

use sentc::keys::StdUser;

async fn example(user: &StdUser)
{
	let list = user.get_groups().await.unwrap();

	//To fetch more groups use pagination and pass in the last fetched item:
	let list = user.get_groups(Some(list.last().unwrap())).await.unwrap();
}

§Encrypt and decrypt in a group

Every group member has access to all group keys and can encrypt or decrypt data for any other group member. To encrypt data, the group uses the most current group key. To decrypt data, the group automatically retrieves the key that was used to encrypt the data.

use sentc::keys::StdGroup;

fn example(group: &StdGroup)
{
	//encrypt a string
	let encrypted_string = group.encrypt_string_sync("hello there £ Я a a 👍").unwrap();

	//decrypt a string. this can be a group obj from another group member
	let decrypted_string = group.decrypt_string_sync(encrypted_string).unwrap();
}

Decrypt will fail when the key that was used is not in the group key vec. The error tells you what key is missing: SentcError::KeyRequired. Just do a key rotation in this case to fetch the key.

§Group rank

The user’s rank in a group determines their level of access. An administrator or creator has full control, while a regular member may have limited privileges such as being unable to accept join requests. Ranks are assigned as numbers ranging from 0 to 4

  • 0 is the creator of a group and has full control
  • 1 is an administrator and has nearly full control, except for removing the creator
  • 2 can manage users: accept join requests, send invites, change user ranks (up to rank 2), and remove group members ( with a rank of 2 or lower)
  • 3 and 4 are normal user ranks. A new member is automatically assigned rank 4. Rank 3 can be used for other actions, such as content management.

To change a user’s rank, you need the Sentc API user ID and assign a new rank number:

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt_from_user: &str)
{
	group.update_rank(jwt_from_user, "user_id_to_update", rank).await.unwrap();
}

If you have your own backend and want to change a user’s rank using a secret token, use this function to obtain the input data for the API. To change the rank, make a PUT request to the following URL with the group ID and the input data from your backend: https://api.sentc.com/api/v1/group/<the_group_id>/change_rank

use sentc::keys::StdGroup;

fn example(group: &StdGroup)
{
	group.prepare_update_rank("user_id_to_update", rank).unwrap();
}

§Invite more user

There are two methods to add more users to a group: by invitation or by join request. When a user is invited or their join request is accepted, the group keys are encrypted using the new member’s most current public key.

§Invite a user

Inviting a user is done by a group administrator (ranks 0-2) to a non-group member. The non-group member can choose to accept or reject the invitation.

Optional, a rank can be set for the invited user.

Get the user public key first.

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt_from_user: &str)
{
	group.invite(jwt_from_user, "user_id_to_invite", user_public_key, None).await.unwrap();

	//with optional rank, in this case rank 1
	group.invite(jwt_from_user, "user_id_to_invite", user_public_key, Some(1)).await.unwrap();
}

A user can get invites by fetching invites or from init the client.

use sentc::keys::StdUser;

async fn example(user: &StdUser)
{
	let list = user.get_group_invites(None).await.unwrap();

	//to fetch the next pages:
	let list = user.get_group_invites(list.last()).await.unwrap();
}

To accept an invitation as user call his function with the group id to accept:

The group id can be got from the GroupInviteReqList

use sentc::keys::StdUser;

async fn example(user: &StdUser)
{
	user.accept_group_invite("group_id").await.unwrap();
}

Or reject the invite

use sentc::keys::StdUser;

async fn example(user: &StdUser)
{
	user.reject_group_invite("group_id").await.unwrap();
}

§Join request

A non-group member can request to join a group by calling this function. A group administrator can choose to accept or reject the request. To request to join a group, call this function with the group ID.

use sentc::keys::StdUser;

async fn example(user: &StdUser)
{
	user.group_join_request("group_id").await.unwrap();
}

To fetch the join requests as a group admin use this function:

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt_from_user: &str)
{
	let list = group.get_join_requests(jwt_from_user, None).await.unwrap();

	//To fetch more requests just pass in the last fetched item from the function:
	let list = group.get_join_requests(jwt_from_user, list.last()).await.unwrap();
}

A group admin can accept the request like this:

Fetch the public key of the user first.

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt_from_user: &str)
{
	group.accept_join_request(jwt_from_user, user_key, "user_id", None).await.unwrap();

	//with optional rank, in this case rank 1
	group.accept_join_request(jwt_from_user, user_key, "user_id", Some(1)).await.unwrap();
}

Or reject it:

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt_from_user: &str)
{
	group.reject_join_request(jwt_from_user, "user_id").await.unwrap();
}

A user can fetch the sent join requests:

use sentc::keys::StdUser;

async fn example(user: &StdUser)
{
	let list = user.get_sent_join_req(None).await.unwrap();

	//to load more use the last item of the pre-fetch

	let list = user.get_sent_join_req(list.last()).await.unwrap();
}

A user can also delete an already sent join request. The group id can be fetched from the get_sent_join_req() function.

use sentc::keys::StdUser;

async fn example(user: &StdUser)
{
	user.delete_join_req("group_id").await.unwrap();
}

§Auto invite

A group administrator can use this function to automatically invite and accept a non-group member, without requiring any additional actions from the new member. This feature can be useful for one-on-one user sessions.

Get the user public key.

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt_from_user: &str)
{
	group.invite_auto(jwt_from_user, "user_id", user_key, None).await.unwrap()
}

§Stop invite

Calling this function will prevent non-group members from sending join requests and group administrators from inviting more users. This feature can be useful for one-on-one user sessions. After automatically inviting the other user, you can use this function to close the invitation process.

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt_from_user: &str)
{
	group.stop_invites(jwt_from_user).await.unwrap()
}

§Get group member

The fetch uses pagination to not fetch all members at once.

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt_from_user: &str)
{
	let list = group.get_member(jwt_from_user, None).await.unwrap();

	//To fetch more use the last fetched member item:

	let list = group.get_member(jwt_from_user, list.last()).await.unwrap();
}

§Delete group member

A group member with a rank higher than 2 (0, 1, 2) can use this function to delete another member with the same or lower rank. However, a member cannot delete themselves using this function.

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt_from_user: &str)
{
	group.kick_user(jwt_from_user, "user_id").await.unwrap();
}

§Leave a group

Every member can leave a group except the creator.

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt_from_user: &str)
{
	group.leave(jwt_from_user).await.unwrap();
}

§Parent and child group

A group can be set as a child of a parent group, creating a hierarchical structure of groups. All members of the parent group are automatically granted access to the child group(s) with the same rank as in the parent group. When a new member joins the parent group, they are automatically added as a member to all child groups. Multiple child groups can also be created:

parent
    child from parent
        child from child from parent
            child from child from parent
    child from parent

To create a child group just call group create in the parent group not in the user scope

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt_from_user: &str)
{
	let group_id = group.create_child_group(jwt_from_user).await.unwrap();
}

If you want to create a child group from your own backend, you can use this function to generate the necessary input data. After generating the data, call your API with a POST request and include the input data. The endpoint for creating a child group is: https://api.sentc.com/api/v1/group/<the_group_id>/child

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt_from_user: &str)
{
	let (input, used_key_id) = group.create_child_group(jwt_from_user).await.unwrap();
}

To get all children of the first level use the getChildren() function in the group object.

It returns a List with the child group id, the child group created time and the parent id.

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt_from_user: &str)
{
	let list = group.get_children(jwt_from_user, None).await.unwrap();

	//to get the 2nd page pass in the last child
	let list = group.get_children(jwt_from_user, list.last()).await.unwrap();
}

§Connected groups

A group can also be a member in another group which is not a child of this group. Connected groups can also have children or be a child of a parent. Groups with access to the connected group got also access to all the child groups. A connected group can’t be member in another group, so only normal groups can be a member in a connected group. Normal groups can’t have other groups as member except their child groups.

A connected group can be created from a normal group.

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt_from_user: &str)
{
	let group_id = group.create_connected_group(jwt_from_user).await.unwrap();
}

To fetch the connected group you can fetch it from the group.

use sentc::keys::StdGroup;
use sentc::group::net::GroupFetchResult;

async fn example(group: &StdGroup, jwt_from_user: &str)
{
	let (data, res) = group.prepare_get_connected_group("connected_group_id", jwt_from_user).await.unwrap();

	assert!(matches!(res, GroupFetchResult::Ok));

	let group = group.done_get_connected_group(data).unwrap();
}

When accessing a child group of a connected group, make sure to load the parent group first which is connected to the user group.

To get all connected groups to a group use the get_groups() function in the group struct. It returns a List of groups with the group id and the group created time.

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt_from_user: &str)
{
	let list = group.get_groups(jwt_from_user, None).await.unwrap();

	//to get the next pages, use the last item.
	let list = group.get_groups(jwt_from_user, list.last()).await.unwrap();
}

Like users, groups can also send join requests to connected groups.

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt_from_user: &str)
{
	group.group_join_request(jwt_from_user, "group_id_to_join").await.unwrap();
}

Groups can also fetch the sent join requests.

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt_from_user: &str)
{
	let list = group.get_group_sent_join_req(jwt_from_user, None).await.unwrap();

	//to load more use the last item of the pre-fetch
	let list = group.get_group_sent_join_req(jwt_from_user, list.last()).await.unwrap();
}

A group can also delete an already sent join request. The group id can be fetched from the get_group_sent_join_req() function.

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt_from_user: &str)
{
	group.delete_join_req("group_id_to_delete", jwt_from_user).await.unwrap();
}

§Child groups vs connected groups, when use what?

The problem with child groups is that it is a fixed structure and can’t be changed in the future.

A connected group can be helpfully if you want to give a group (and all its parents) access to another group (and all its children). This can be used to connect resources and users together, e.g.:

  • user in department groups (hr, marketing, development)
  • resources like customer, employee data, devops secrets
  • let dev manager access group employee data and devops secrets and marketing access customer.
  • Inside each department group there are multiple child groups for each sub department. If the manger is in the parent group, he/she can access every subgroup

The recommended approach is to use normal groups for user and connected groups for resources.

parent
    child from parent                       -->              connected group
        child from child from parent                           child from connected group
            child from child from parent
    child from parent

§Key rotation

A group can have multiple encryption keys at the same time. Key rotation is the process of generating new encryption keys for a group while still allowing the use of the old ones. This is done on the server side, but the server does not have access to the clear text keys, making it suitable for large groups as well.

Key rotation can be useful when a member leaves the group, ensuring that all new content is encrypted using the newest key.

The user who starts the rotation can also sign the new keys. When the other member finish the rotation, the signed keys can be verified to make sure that the starter is the real user.

§Key rotation start

To start the rotation call this function from any group member account.

In the rust version, you need to pass in:

  • the jwt from the user
  • user id that started the rotation
  • and a ref to the user to get the keys

You can get everything from the user struct as it is shown below.

The function will return a result enum of the key fetch that tells the user if they need to fetch keys from the backend if the key was encrypted by a user key that is not found on the client. Normally this should not happen because the user did the rotation with their newest key.

use sentc::keys::{StdGroup, StdUser};

async fn example(group: &StdGroup, user: &StdUser)
{
	//first prepare to check if there are keys missing for the user
	let res = group.prepare_key_rotation(user.get_jwt().unwrap(), false, user.get_user_id().to_string(), Some(user), None).await.unwrap();

	//end the rotation by fetching the new key
	let data = match res {
		GroupKeyFetchResult::Ok(data) => data,
		_ => {
			panic!("should be no missing key or done");
		}
	};

	//decrypt the newest group key by the user key.
	group.done_fetch_group_key_after_rotation(data, Some(user), None).unwrap();
}

Rotation with signing the public group key:

use sentc::keys::{StdGroup, StdUser};
use sentc::group::net::GroupKeyFetchResult;

async fn example(group: &StdGroup, user: &StdUser)
{
	//first prepare to check if there are keys missing for the user
	let res = group.prepare_key_rotation(user.get_jwt().unwrap(), true, user.get_user_id().to_string(), Some(user), None).await.unwrap();

	//end the rotation by fetching the new key
	let data = match res {
		GroupKeyFetchResult::Ok(data) => data,
		_ => {
			panic!("should be no missing key or done");
		}
	};

	//decrypt the newest group key by the user key.
	group.done_fetch_group_key_after_rotation(data, Some(user), None).unwrap();
}

The new keys will be created on your device, encrypted by the starter public key, and sent to the API. The API will encrypt the new group keys for all other members, but the API still doesn’t know the clear text keys and can’t use them because the new keys are encrypted by an ephemeral key that is only accessible to the group members.

It doesn’t matter how many members are in this group because the user devices are not doing the encryption for every member.

§Key rotation finish

To get the new key for the other member just call this function for all group member:

use sentc::keys::{StdGroup, StdUser};
use sentc::group::net::{GroupFinishKeyRotation, GroupKeyFetchResult};

async fn example(group: &StdGroup, user: &StdUser)
{
	//This fn checks if the user needs to fetch the newest user key. if no continue
	let res = group.prepare_finish_key_rotation(user.get_jwt().unwrap(), Some(user), None).await.unwrap();

	//check if the user needs to fetch keys first
	let data = match res {
		GroupFinishKeyRotation::Ok(data) => data,
		_ => {
			panic!("Should be ok")
		}
	};

	//This function will fetch all new group keys
	let res = group.done_key_rotation(user.get_jwt().unwrap(), data, None, Some(user), None).await.unwrap();

	//fetch each new key after all rotations
	for key in res {
		let data = match key {
			GroupKeyFetchResult::Ok(data) => data,
			_ => panic!("should be ok"),
		};

		group.done_fetch_group_key_after_rotation(data, Some(user), None).unwrap();
	}
}

This will fetch all new keys for a group and prepares the new keys.

§Key rotation with own backend

If you want to control the rotation from your own backend, just call this function to start the rotation:

use sentc::keys::{StdGroup, StdUser};

fn example(group: &StdGroup, user: &StdUser)
{
	let input = group.manually_key_rotation(false, user.get_user_id().to_string(), Some(user), None).unwrap();
}

and call this endpoint to start the rotation with a post request: https://api.sentc.com/api/v1/group/<group_id>/key_rotation

Still use the finishKeyRotation function to finish the rotation.

§Re invite

If there is an error during the key rotation, the corresponding user won’t get the new keys. This can happen if the user already done a user key rotation and the keys are not correctly created.

Users can be re invited to a group. It is almost the same process as the invite but this time the user keeps the rank.

use sentc::keys::StdGroup;

async fn example(group: &StdGroup)
{
	group.re_invite_user("user_id", user_public_key).await.unwrap();
}

§Public group information

Only the newest public key is used. You can just fetch the newest group public key.

use sentc::net_helper::get_group_public_key;

async fn example()
{
	let public_group_key = get_group_public_key("base_url", "app_token", "group_id").await.unwrap();
}

§Delete a group

Only the creator (rank 0) or the admins (rank 1) can delete a group.

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt: &str)
{
	group.delete_group(jwt).await.unwrap();
}

§Backend endpoints

To create and delete groups from your backend the jwt of the creator is always required. If the jwt is not available in some situations you can use the following endpoints to call it with your secret token.

  • Deleting a group with a delete request: https://api.sentc.com/api/v1/group/forced/<group_id_to_delete>
    • This endpoint will delete the group
  • Creating a group with a post request: https://api.sentc.com/api/v1/group/forced/<creator_user_id>
    • use the prepareGroupCreate function in the group section to get the encrypted keys for the creator and call this endpoint with the returned string
    • This endpoint will return the group_id
  • Creating a child group with a post request: https://api.sentc.com/api/v1/group/forced/<creator_user_id>/<parent_group_id>/child
    • do the same as for creating a normal group but use prepareCreateChildGroup in the parent group to get the decrypted keys
  • Create a connected group with a post request: https://api.sentc.com/api/v1/group/forced/<creator_user_id>/<connected_group_id>/connected

§Encrypt for a group

When encrypting content for a group, the content will be encrypted using the group’s current key. In the event of a key rotation, the new group key will be used to encrypt new content, while the previous key can still be used to decrypt previously encrypted content.

Sentc will handle key management for you, determining which key should be used for encryption and which key should be used for decryption.

§Encrypt raw data

Raw data are bytes (&u8).

use sentc::keys::StdGroup;

fn example(group: &StdGroup, data: &[u8])
{
	let encrypted = group.encrypt_sync(data).unwrap();
}

§Decrypt raw data

For groups this is the same way around like encrypting data. Every group member can encrypt the data.

use sentc::keys::StdGroup;

fn example(group: &StdGroup, data: &[u8])
{
	let decrypted = group.decrypt_sync(data, None).unwrap();
}

§Encrypt strings

Encrypting strings is a special case, as it requires converting the text to bytes using an UTF-8 reader before encryption.

To simplify this process, Sentc offers string encryption functions that handle this conversion for you.

use sentc::keys::StdGroup;

fn example(group: &StdGroup, data: &str)
{
	let encrypted = group.encrypt_string_sync(data).unwrap();
}

§Decrypt strings

The same as decrypt raw data but this time with a string as encrypted data.

use sentc::keys::StdGroup;

fn example(group: &StdGroup, data: &str)
{
	let decrypted = group.decrypt_string_sync(data, None).unwrap();
}

§Sign and verify the encrypted data

Sentc offers the ability to sign data after encryption and verify data before decryption. This ensures the authenticity of the encrypted data and protects against potential tampering.

§Sign

For sign, the newest sign key of the user is used.

For the rust version you need to get the sign key from the user.

use sentc::keys::StdGroup;

fn example(group: &StdGroup, data: &str)
{
	let encrypted = group.encrypt_string_with_sign_sync(data, user_sign_key).unwrap();
}

§Verify

For verify, the right verify key needs to be fetched first.

use sentc::keys::StdGroup;

fn example(group: &StdGroup, data: &str)
{
	let decrypted = group.decrypt_string_sync(data, Some(user_verify_key)).unwrap();
}

§Searchable encryption

When the data is fully end-to-end encrypted, nobody even the server can decrypt and read / analyse the data. This makes it very hard to query the data without loading everything all at once, decrypt in the client and then fulfill the query.

With Sentc you can create searchable encryption from your keywords.

Hmac is used to create hashes from the keywords in the client, which can then be searched. Hmac makes it also harder to re calculate the hash.

Searchable is only possible for groups.

You can choose if you want to hash the full keyword or each character.

Hashing the full word can be helpful if you want to store credit-card numbers. In this case you will only find a match if you pass in the full number

When hashing each character you can search for similar results. You can also set a limit of how many characters will be hashed. For no limit, all characters will be hashed. It will only find words from the start to bottom, the word smart and smith will be

  • matched for: s or sm
  • but not for: m or t.

Unlike the sortable encryption, this technic can also be used to encrypt sensible data. For sortable encryption, only the first 4 letters are encrypted, here the full string can be encrypted.

This is only a one-way process. You can’t get the plain text back. You can use it alongside with symmetric group encryption, encrypt the data with the group key and also hash the data. The group members are still able to decrypt the data, and you can search it in your backend.

§Create

You get a list or any array of hashes back. This contains the hash of each letter combination of your string.

You can store the hashes in your database in a different table like this:

item_idhash
your id to an itemhash of the item
your id to an itemanother hash of the item
your id to another itemhash of the other item

The relationship should be 1:n (one to many) because one hash should only belong to one item but an item can have multiple hashes.

The length of a hash is 32 bytes, so a varchar(32) should be great.

For sql databases, bulk insert (multiple rows at once insert) should be used for the hashes.

use sentc::keys::StdGroup;

fn example(group: &StdGroup, data: &str)
{
	let hashes = group.create_search_raw(data, false, None).unwrap();
}

To get more information about how the value is encrypted, you can use this function instead:

use sentc::keys::StdGroup;

fn example(group: &StdGroup, data: &str)
{
	let out = group.create_search(data, false, None).unwrap();
}

§Search a value

The sdk will create a hash with the same group key for your search term, and now you can check it in your database with the hashes table. If you are using primary keys for the hash and the item id the search is very quick.

use sentc::keys::StdGroup;

fn example(group: &StdGroup, data: &str)
{
	let hash = group.search(data).unwrap();
}

Now you can search it in your db like this:

SELECT <your_columns>
FROM <your_item_table> i, <your_hash_table> ih
WHERE
	ih.hash = ?
  AND ih.item_id = i.id
ORDER BY...LIMIT...

For this query you will get a list of all matched hashes.

§Options

You can also limit the number of hashes of a word (e.g. only the first 4 combinations) or just hash the full word without letter combinations. This works for both, the raw and the normal functions.

§Full word hash

use sentc::keys::StdGroup;

fn example(group: &StdGroup, data: &str)
{
	let hashes = group.create_search_raw(data, true, None).unwrap();
}

Now the length of the hashes is only one.

§Hash a limit letter combination

use sentc::keys::StdGroup;

fn example(group: &StdGroup, data: &str)
{
	let hashes = group.create_search_raw(data, false, Some(4)).unwrap();
}

Now the length of the hashes is maximal 4.

§Going further

You can also set a boolean flag in your hashes table for the last hash of a word. The last item in the hashes list is also the hash of the exact word.

This helps to do queries where you need the equal value and not all values like this.

item_idhashlast
your id to an itemhash of the itemfalse
your id to an itemanother hash of the itemtrue
your id to another itemhash of the other itemfalse

If you want to only got the exact values test if the last is true.

With search and sortable encryption you got now everything you need to query your data without decrypting it first.

§Sortable encryption

When the data is fully end-to-end encrypted, nobody even the server can decrypt and read / analyse the data. To do range queries like sort table by last name the server must know the decrypted value. The encryption only works in groups.

With the sortable encryption it is not needed anymore. The encrypted produces numbers which follows the order of the plaintext and can be used with any database or backend.

Like: encrypt(1) < encrypt(2) < encrypt(3) < encrypt(5000)

Now it is possible to do range queries or sort rows without decrypt it.

The encryption is not as secure as the symmetric or asymmetric encryption. This is why sentc never encrypt the whole plaintext.

You can encrypt numbers or strings. Numbers are fully encrypted, for strings only the first 4 characters will be encrypted and the rest gets ignored.

Use this technic only in combination with the symmetric encryption to encrypt a value symmetrically so the user can decrypt it and also encrypt it with the sortable encryption to do range queries in your backend.

§Encrypt a number

The maximum number to encrypt is 65532.

use sentc::keys::StdGroup;

fn example(group: &StdGroup)
{
	let a = group.encrypt_sortable_raw_number(262).unwrap();
	let b = group.encrypt_sortable_raw_number(263).unwrap();
	let c = group.encrypt_sortable_raw_number(65321).unwrap();

	//a < b < c
}

To get more information about how the value is encrypted, you can use this function instead:

use sentc::keys::StdGroup;

fn example(group: &StdGroup)
{
	let out = group.encrypt_sortable_number(262).unwrap();
}

§Encrypt a string

Only the first 4 characters will be used to encrypt the string the rest will be ignored.

Strings with umlauts or other non english character are not supported. Alternative you can use the english version like ö ot oe

But you can write your own function that creates a number and encrypt the number.

use sentc::keys::StdGroup;

fn example(group: &StdGroup)
{
	let a = group.encrypt_sortable_raw_string("abc", None).unwrap();
	let b = group.encrypt_sortable_raw_string("dfg", None).unwrap();
	let c = group.encrypt_sortable_raw_string("hij", None).unwrap();

	//a < b < c
}

To get more information from the encrypted strings use this function:

use sentc::keys::StdGroup;

fn example(group: &StdGroup)
{
	let out = group.encrypt_sortable_string("abc").unwrap();
}

§File handling

File handling will be available after the beta.

Handling large encrypted files can be difficult, especially in the browser.

Large files are generally too big to encrypt all at once and can potentially overload system memory. To solve this issue, one solution is to use a stream to encrypt and decrypt files one piece at a time. However, browsers cannot send file streams through requests.

Another solution is to chunk the file into smaller pieces and encrypt each piece before sending it to storage. This allows encrypted files to be sent from the browser, but requires managing multiple files instead of just one. In addition to handling uploads, file deletion must also be managed, including deleting the individual pieces.

::: tip Sentc solution

Sentc offers a solution for handling large encrypted files. In the client, Sentc chunks the file and encrypts each piece. These encrypted pieces are then sent to our API storage or your storage.

We save all the part IDs associated with your file, allowing you to fetch the complete file from our backend as if it were a single file. Additionally, you can delete the file as if it were a single file, and Sentc will manage the deletion of the individual encrypted pieces.

§Encrypt and upload a file

With Sentc, files can be encrypted for a group or for a single user. We recommend encrypting files for a group, as this allows all group members to download and decrypt the file.

For each file, Sentc creates a new key that is used for encryption. To encrypt and upload a file for a group, follow these steps:

It is important to store the file id to fetch the file later.

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt: &str, file: File)
{
	let output = group.create_file_with_file(jwt, file, None, None, None).await.unwrap();
}

For another user:

use sentc::keys::StdUser;

async fn example(user: &StdUser, file: File)
{
	let output = user.create_file_with_file(file, Some(reply_id), Some(reply_public_key), None, None, false).await.unwrap();
}

Create a file with a path:

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt: &str, path: &str)
{
	let output = group.create_file_with_path(jwt, path, None, None).await.unwrap();
}

For another user:

use sentc::keys::StdUser;

async fn example(user: &StdUser, path: &str)
{
	let output = user.create_file_with_path(path, Some(reply_id), Some(reply_public_key), None, false).await.unwrap();
}

To also sign a file, set the ‘sign’ parameter to ‘true’ in the function. This will use the user’s sign key. Note that this is not necessary when handling files only within your application and not from any other apps.

When downloading and verifying the file, you will also need to store the user ID to fetch the right verify key.

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt: &str, file: File)
{
	let output = group.create_file_with_file(jwt, file, None, None, Some(sign_key)).await.unwrap();
}

For another user:

use sentc::keys::StdUser;

async fn example(user: &StdUser, file: File)
{
	let output = user.create_file_with_file(file, Some(reply_id), Some(reply_public_key), None, None, true).await.unwrap();
}

To see the actual upload progress pass in the create file function a closure:

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt: &str, file: File)
{
	let output = group.create_file_with_file_and_upload_progress(jwt, file, None, None, None, |progress| {
		//do something with the progress
	}).await.unwrap();
}

§Download and decrypt a file

To download a file, simply use its file ID. The file key may be encrypted using either another created key or a group key. The file creator will always provide you with the master key ID.

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt: &str, file: File)
{
	let output = group.download_file(jwt, file, "file_id", None, None).await.unwrap();
}

Download file for another user:

use sentc::keys::StdUser;

async fn example(user: &StdUser, file: File)
{
	let output = user.download_file(file, "file_id", None, None).await.unwrap();
}

To also verify the file by put in the right verify key. Make sure you save the user id from the creator of the file when uploading a file.

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt: &str, file: File)
{
	let output = group.download_file(jwt, file, "file_id", Some(verify_key), None).await.unwrap();
}

To see the actual download progress pass in the download file function a closure:

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt: &str, file: File)
{
	let output = group.download_file_with_progress(jwt, file, "file_id", |progress| {
		//do something with the progress
	}, None, None).await.unwrap();
}

§Delete a file

Just pass in the file id of the file to delete.

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt: &str)
{
	let output = group.delete_file(jwt, "file_id").await.unwrap();
}

§Setting up your storage

In the App options, you can choose to use your own storage for file upload and download. By default, the SDK uses sentc storage, and you are charged per GB per month for its usage.

If you have your own storage solution, such as AWS S3, you can simply update the delete, download and upload URLs to point to your own storage. This will allow all file parts to be uploaded and downloaded directly from your storage.

If a file is deleted, we will call your backend storage to delete the corresponding file. The delete process can be stacked to delete multiple files at once.

In summery:

  1. Set up your own upload and download endpoints in the app options within the SDK.
  2. Configure your upload endpoint to receive multiple parameters through the URL.
  3. Call the sentc API to register the file part, and you will receive an ID that can be used to delete the file part.
  4. Set up the delete endpoint and an optional token in your app’s file options within the dashboard.

In rust, you need to pass in the url with the parameter:

use sentc::keys::StdGroup;

async fn example(group: &StdGroup, jwt: &str, file: File)
{
	let output = group.download_file(jwt, file, "file_id", None, Some("file_url")).await.unwrap();
}

We use the same URL for both upload and download, but with different HTTP methods:

  • Upload: Method post
  • Download: Method get

To update your URL, simply set the file part URL in the options. The uploader will automatically upload the parts to the new URL, and the downloader will attempt to download the parts from the new URL.

Please ensure that you transfer your data to the new URL.

§When uploading file parts to your url, register the file part at sentc api

Call this endpoint when the upload is done:

  • https://api.sentc.com/api/v1/file/part/<session_id>/<file_part_sequence>/<end>/<user_id>

This endpoint needs your secret token and should only be called from your backend. See own backend for sending the token as header.

Header name: x-sentc-app-token
Header value: <your_app_token>
  • session_id is the id of the file upload session, this is a string.
  • file_part_sequence is the sequence of the file part when downloading and decrypting the file. if this is wrong then the file can’t be decrypted anymore.
  • end is a boolean. Pass in false if the file upload has not finished yet or true if it is.
  • user_id is the user that uploaded the file.

The sdk will call your endpoint with these values in the url as parameter and the user id from the user jwt or elsewhere. A request might look like:

  • https://your_url.com/<session_id>/<file_part_sequence>/<end>
  • or https://your_url.com/abc_123/0/false
  • or https://your_url.com/abc_123/1/true

Just extract the values and call the sentc api to register the file part, so sentc can download the file. In the example above:

  • https://api.sentc.com/api/v1/file/part/abc_123/0/false/<user_id>
  • and https://api.sentc.com/api/v1/file/part/abc_123/1/true/<user_id>

§After calling the sentc api you will get back the file part id

This id is used to fetch and delete a part. Please store the id or rename your file part to this id.

Return the success result as json to the sdk: {"status":true,"result":"Success"}.

§Alternative workflow

You can also call the sentc api first to register a part and then read the request body. Then you will get the right id, and you can name your file correctly.

§Set to delete endpoint for file parts

This endpoint will be called with a post request and the deleted file part names in the body as json array:

[
	"name_0",
	"name_1",
	"name_2"
]

You can also set a token for us, so you know that the request comes really from our api to delete the files.

§When downloading a part the part id is in the url

The sdk will call your endpoint with a get request and the part id in the url. And except the encrypted file part as bytes.

https://your_url.com/<part_id>

§Application

§Create an account

  1. Got to https://api.sentc.com/dashboard/register and fill in the account information:
  • Email
  • First name
  • Last name
  • Company (optional)
  1. Choose a password
  2. Confirm the password
  3. Fill out the captcha to prove that you are not a bot
  4. Click register

After registration, you will receive an email. Please click on the link in the email to verify your email address.

Then you are ready to create applications.

§Create an app

  1. When you are on the main dashboard page, click on the “New App” button in the right corner.
  2. Choose an app name (the name will be displayed on the dashboard to make it easier to find your app).
  3. Optionally, you can change the app options or file options. To change the app options, click on the app options name and then the options panel will open. The same goes for file options. By default, files are disabled and only user register and user delete are accessible with your secret token. See “App options” for more information.
  4. After creating the app, you will receive your app tokens (public and secret) and jwt keys (sign and verify). You can download every important key and token as a .env file.

§App tokens and keys

After registration, you will get your app tokens and the jwt keys.

§Jwt keys

With the jwt keys you can create a jwt which is valid for your app at the sentc api (with the sign key) or verify a jwt (with the verify key). The jwt is structured the following:

{
    aud: string,
    sub: string,
    exp: number,
    iat: number,
	group_id: string,
	fresh: boolean
}
  • aud is the user id
  • sub is the device id
  • group_id is the user device group (this value can be ignored)
  • fresh, after login the user will get a fresh token. When the tokens refresh, the jwt is not fresh anymore. A fresh jwt is needed to delete a user, but the sdk will log in the user again before delete to get a fresh jwt

§App tokens

The public app token is used to access the API via the frontend, while the secret token is used for backend access. After creation, app tokens are hashed and cannot be recovered. To renew your app tokens, please remember to update the public app token for your SDK as well.

§App options

With app options, you can control which token can access which endpoint. By default, the public token can access every endpoint except for register and delete user. As Sentc only stores the required data, which includes only the username and encrypted keys, you may require additional information from your users, such as an email address or their full name.

To change the options, simply click on the row of the endpoint and choose public, secret, or block (which means no token can access this endpoint).

Additionally, you can choose other quick options by clicking on the “LAX” button to allow the public token access to all endpoints.

§App file options

By default, file handling is disabled.

However, if you choose to use the Sentc API storage option, no additional configuration is required on your end.

§Own backend

Using your own backend enables you to store files on your own storage system, so you don’t need to pay for our storage services. To use this option, please set the files delete endpoint on your backend. We will call this endpoint with a delete request, passing the names of the deleted file parts in a JSON array in the request body.

["name_0", "name_1", "name_2"]

Each file is divided into multiple parts, each with its own unique ID. These IDs are passed in an array.

As we use a worker to delete files, multiple file parts can be deleted at once.

You can also set a token to ensure that the delete request comes from the Sentc API for your files.

For more information about file handling in Sentc, please refer to the Files section.

§Groups (Beta)

To work with others on an app, there are groups where all group member got access to the app secrets, but only high rak member can edit or delete apps.

An app can be created in a group just like in your account.

  1. Login to your dashboard
  2. In the upper left corner click on the GROUPS tab
  3. Click on NEW GROUP to create a new group
  4. Optional give the group a name and a description
  5. Now the group shows up in your dashboard

To create an app in this group use the NEW APP IN GROUP button

§Manage member

  • To go to the member click on the member icon in the top right corner next to the group name.
    1. In the member list click INVITE MEMBER
    2. Pass in the user id and optional a rank of the user. The rank can be changed later on. The user id is in the user’s menu (the cog symbol in the right corner)
    3. Click on invite. Now the member is successfully added to your member list.
  • To kick a member
    1. click on the pencil icon of the corresponding member
    2. click on kick member
    3. In this window you can also change the rank. This is only possible when you are the admin of this group.

To change the group name or description, go to the pencil icon next to the member icon. In this window you can also delete the group.

§Advanced

§The Sentc Protocol

The Sentc protocol is designed for asynchronous, long-running communication, specifically optimized for groups of users. It ensures that users in groups can decrypt the entire communication, even if they join years after it has started.

§Perfect Forward Secrecy (PFS)

In the Sentc protocol, Perfect Forward Secrecy (PFS) is not implemented in the core for specific reasons.

The first challenge with PFS is that it requires synchronous communication, where all users need to be online simultaneously. While this approach works well for live-streaming video or TLS (Transport Layer Security), where every member must be online at the same time, it poses difficulties for asynchronous communication. The Signal-protocol solve this problem by using pre generated key pairs and use them if the user is offline.

The second challenge with PFS is that new members of a group are unable to decrypt past communications, or at least cannot decrypt them at a given time. This can be problematic, especially in scenarios such as a knowledge wiki within a company or chat communication, where new employees may be unable to access important information for months.

Sentc tackles this problem by implementing key rotation in groups. Each member of the group receives new keys while retaining the ability to decrypt using the old keys. When encrypting, the new key is used. While this solution may not be perfect for one-on-one or live communication, it provides a suitable approach for asynchronous groups.

§Overview

Sentc utilizes a combination of symmetric and asymmetric encryption algorithms, as well as signing and verification mechanisms.

It’s worth noting that the underlying algorithm used in Sentc can be changed in the future. Despite such changes, data encrypted with the previous algorithm will still be able to decrypt, ensuring backward compatibility. However, any new data will be encrypted using the new algorithm. This flexibility is particularly valuable as it allows for future-proofing the system against potential quantum attacks.

§The structure

Here are the actual used algorithm.

  • symmetric for the data encryption and decryption.
    • aes-gcm 256 bit
  • asymmetric for the symmetric key exchange with static key pairs.
    • ecies with x25519
    • CRYSTALS Kyber 768
    • Hybrid with ecies and Kyber (default)
  • sign and verify for data integrity
    • ed25519
    • CRYSTALS Dilithium 3
    • Hybrid with ed25519 and Dilithium (default)
  • hmac for searchable encryption
    • hmac-sha 256
  • password hashing
    • argon2 with argon2id
  • sortable encryption
    • ope

§User

A user is defined as a group of devices. The user-group operates using the same mechanism as a regular group, which will be explained in detail below. The device’s public key is utilized during the creation of the group.

§Device

§Register

A device in Sentc is associated with an identifier, also known as a username, and a password. The password can be either user-generated or securely generated and stored on the client device.

To protect user information, the identifier is hashed on the API side, ensuring that no sensitive user data is exposed. Additionally, the password is designed never to leave the device.

Each device is equipped with the following components:

  1. Symmetric Master Key: This key is static but can be changed when necessary.
  2. Asymmetric Key Pair: Each device possesses a static (yet changeable) asymmetric key pair, comprising a public key and a private key.
  3. Sign and Verify Key Pair: Similarly, each device has a static (but changeable) sign and verify key pair.

Both the private asymmetric key and the sign key are encrypted using the device’s master key, providing an additional layer of security.

Regarding the password, it is hashed using password hashing algorithms, with the current algorithm being argon2.

§Argon2 password hashing
  1. Create a client random value (crv) (16 bytes for argon2)
  2. Generate a salt with crv.
    • The salt contains a padded string (length 200 chars) and the crv.
    • Hash the salt with sha 256
  3. Derived a 512 bit long key from the password and the salt
    • use the first half of the derived key as encryption key and the second half as auth key
    • the auth key will be hashed as well
  4. Finally, encrypt the master key with the first half of the derived key, the encryption key, via aes-gcm

The following is sent to the server:

  • user group data (more see group creation)
  • encrypted master key
  • device identifier (not hashed at this moment)
  • what derived key algorithms was used
  • client random value
  • hashed auth key
  • public key
  • encrypted private key
  • what algorithms has the asymmetric encryption key pair
  • verify key
  • encrypted sign key
  • what algorithms has the sign/verify key pair

The identifier is hashed on the server via sha 256

§Login

Login is split into three tasks.

§Prepare the login

During the login process in Sentc, the user first sends the device identifier to the server and receives the corresponding salt. The salt is generated on the server side. Here’s how the process works:

  1. Device Identifier Submission: The user submits the device identifier to the server.

  2. Salt Retrieval: If the identifier exists in the server’s records, the server generates the salt using the client’s random value.

    • Existing Identifier: If the identifier exists, the server generates the salt based on the client’s random value and the matching device registration data.
    • Non-existing Identifier: If the identifier does not exist, a false identifier (for security purposes) is used in the padded string, and a generic client random value is utilized to generate a false salt.
  3. Salt Transmission: Once generated, the salt is sent back to the client, completing the login preparation stage.

§Finish the login

At the client the salt is used to derive the encryption and auth key via the password hashing algorithms. The auth key is not hashed and will be sent to the server. The server hashes the auth key and checks if the hashed key exists. If not then the identifier or the password may be wrong.

It is important to not just return false if the identifier not exists to impede password brute force attacks.

If the auth key exists then the server will create a login challenge. This is a randomly created string which is encrypted by the users device public key (not the user public key) on the server. The device keys including: encrypted private keys and the encrypted master key and the challenge are returned to the client.

The master key will be decrypted by the encryption key from the password and the decrypted master key decrypts the private keys.

The login challenge will then be decrypted with the decrypted private device key and send back to the server to verify that the user not only got access to the auth key but also to the master key.

§Verify Login

After the api verifies the login the server will create a json web token (jwt) and return the jwt, a refresh token and the user group keys.

With the device private key, the group keys are decrypted.

The jwt can be used to authenticate with the server.

There is no key rotation for a single device but if a device get lost or compromised the device can be deleted and a key rotation can be done in the user-group for the other devices.

§Safety number / public fingerprint

A user can create a safety number (commonly known as public fingerprint) from its own verify key and another user. The verify-keys get combined and hashed. The hash is then shown as base64 encoded string.

The user which id comes first in the alphabet will always be the first in the hash.

§Verify public key

The public key of a user is used to encrypt the group keys. To make sure to use the right public key of the real user a public key can be verified. The safety number can identify the right verify key and the verify key can verify the public key.

When a user was registered, the user group verifies the public key of the user. After a user key rotation the new public key will also be verified by the new verify key (in this case a new safety number is needed).

It is stored on each device if a public key was verified before. To only allow verified user public keys for groups it can be checked in the client before inviting a new user.

§Groups

Groups use symmetric keys to encrypt content among the members. Every user inside a group can encrypt and decrypt.

There are also a public/private key pair for each group. For user groups there is also a sign/verify key pair. The private key is encrypted by the group key.

The symmetric group key is encrypted by the public key of the creator of the group.

§Adding more user to a group

Adding more users to a group can be done via join req from the user to the group or invite from the group to the user. For inviting a user or accepting a join req, all group symmetric keys are encrypted by the users public key in the client and then send encrypted to the server. When the new member wants to load the group, the group keys are decrypted by the users private key.

When a user gets kicked out a group, all the encrypted group keys for this user will be deleted as well. It is recommended to do a key rotation after a user leaves to encrypt newer content with keys that the old user not have.

§Child and connected groups

When inviting a user, the newest user public key is used to encrypt the group keys. When creating a child group, the parent group public key is used to encrypt the group keys. All users for the parent group got access to the private key (by the group key) to decrypt the child group key. A parent group is like a member in the child group.

For connected groups the group is also a member in the connected group.

§Key rotation

Using just one static symmetric key is not secure. Sentc provides a key rotation mechanism which is done on the server to relieve the clients.

§Starting the rotation

This is all done in the client.

  1. A new symmetric key, public/private key pair, and for user groups a new sign/verify key pair will be created.
  2. Like group create, the private key will be encrypted by the new group key
  3. An ephemeral key will be created to encrypt the new group symmetric key
  4. The ephemeral key is then encrypted by the previous group key
  5. Optional sign the encrypted group key with the user sign key
  6. For the invoker the group key is also encrypted by its public key like group invite
  7. Send the following to the server:
    • encrypted private key
    • public group key
    • encrypted group symmetric key by the ephemeral key
    • encrypted group key by the invoker public key
§Server key rotation

This section is done on the server.

  1. Insert the group keys
  2. Start a background worker to do the key rotation
  3. At the worker, fetch the encrypted ephemeral key
  4. Fetch the newest public key of first 100 group member (order by joined time)
  5. Encrypt the ephemeral key (which was encrypted by the previous group key before) with the public key of each user and store the encrypted ephemeral key by the public key.
  6. Continue until all member and parent / connected groups are done
  7. Delete the encrypted ephemeral key which was encrypted by the previous group key to not leak it for ex-member.
§Finish the key rotation for the other member in the client

This process is done in the client of the other member.

  1. Get the user private key (or group private key in case of parent or connected group), the user public key and the previous group key
  2. If the encrypted group key was signed then the member can verify the group key
  3. Decrypt the encrypted ephemeral key with the users private key (the pair key from the used public key at the server)
  4. Decrypt the already decrypted ephemeral key but this time with the previous group key
  5. Decrypt the new encrypted group key with the now decrypted ephemeral key
  6. Encrypt the new group key with the users public key
  7. Send the new encrypted group key by the public key to the server to store it like the keys before

The process for parent and connected groups is the same as for member, but the group public key is used instead of a user public key.

§File

A file is chunked into parts (each 4 m-bytes). Each part got its own symmetric key.

  1. Create a symmetric key. This key is the starting key and is directly linked to the file and will be deleted when file is deleted. This key is encrypted by a group key if the file is created in a group, or with the user public key if not.
  2. Encrypt the first chunk with the new key and create a new symmetric key. Use the initial key to encrypt the created file key.
  3. Use the next key to encrypt the next part and also create another key and encrypt this key with the key from the part before.
  4. Do this until all chunks are done

To decrypt a file:

  1. Use the initial file key to decrypt the first part and the key for the next part
  2. Use the decrypted key to decrypt the next part and its key for the following part
  3. Do this until all chunks are decrypted

The file part keys are stored encrypted in the file chunk to load the files independent of sentc backend.

§Database with encrypted values

Like it was mentioned in searchable and sortable you can still use your database to query data to the customer without the need to decrypt it first and then doing queries like searching for a value, getting the exact results or returning a sorted list of values.

Use searchable encryption to get either the exact data match or multiple data to a searched value and sortable encryption to do range queries like ORDER BY name.

Because sortable encryption won’t encrypt your full data it should not be used for exact matches.

The best thing is that you don’t need to modify your database or just different functions. Both technics can be used with the native database query functions like you would do with not encrypted data.

The example below is a relational database like mysql, sqlite or postgres but this will also work for nosql databases.

§The tables

Use a normal table for your data like you would do without encryption.

Table: user:

idfirst_namelast_nameage
123JonSnow24
124JohnSnowing55
125JohnnyDepp60

In the real world the data is end-to-end encrypted in your group. The problem, you can’t do anything with it, except storing them.

§Searchable

Create a 2nd table for the search hashes of each column you want to query and link it to your users table.

Table: user_hash:

item_idhash
123hash of Jon
123another hash of Jon
124hash of John
125hash of Johnny

Now create the hashes like this:

use sentc::keys::StdGroup;

fn example(group: &StdGroup)
{
	let hashes_jon = group.create_search_raw("Jon", false, None).unwrap();
	let hashes_john = group.create_search_raw("John", false, None).unwrap();
	let hashes_johnny = group.create_search_raw("Johnny", false, None).unwrap();
}

You can also set a boolean flag in your hashes table for the last hash of a word. The last item in the hashes list is also the hash of the exact word.

This helps to do queries where you need the equal value and not all values like this.

item_idhashlast_hash
123hash of Jonfalse
123another hash of Jontrue
124hash of Johnfalse
125hash of Johnnyfalse

If you want to only got the exact values test if the last is true.

§Sort/Order able

If you also want to query the last name create a second table for the hashes of the last name or create a column with a flag that identifies to what column the hash is for, for the id.

To do range queries expand the users table by a column of a value that you want to do the range query. Like if you want to ORDER BY first_name, then create another column with the sortable first_name.

idfirst_namelast_nameageorder_first_name
123encryptedencryptedencrypted1267
124encryptedencryptedencrypted1268
124encryptedencryptedencrypted1269

Now create the sortable column like this:

use sentc::keys::StdGroup;

fn example(group: &StdGroup)
{
	let sort_jon = group.encrypt_sortable_raw_string("Jon", None).unwrap();
	let sort_john = group.encrypt_sortable_raw_string("John", None).unwrap();
	let sort_johnny = group.encrypt_sortable_raw_string("Johnny", None).unwrap();
}

§Query

To get now the data just use the normal database queries.

To get all users order by name:

SELECT id, first_name, last_name, age
FROM users
ORDER BY order_first_name

This data can then decrypt by the group key.

To get the exact data you need to create a hash in the client first and then search it:

use sentc::keys::StdGroup;

fn example(group: &StdGroup)
{
	let hash = group.search("jo").unwrap();
}
SELECT id, first_name, last_name, age
FROM users u,
	 user_hash uh
WHERE u.id = uh.item_id
  AND hash = ?

The result would be all three users because everyone begins with jo. The is equal to sql LIKE queries.

To get exact values just check if it is the last hash (of the full word).

SELECT id, first_name, last_name, age
FROM users u,
	 user_hash uh
WHERE u.id = uh.item_id
  AND last_hash = TRUE
  AND hash = ?

For the hash of John only the data with id 124 will be returned but not johnny (id 125).

Now you still get the ability to do search queries and exact matches. With createSearch option full you can’t do searching.

§Own backend processing

For each endpoint, you can specify which token is required to access it in the app options.

By default, all endpoints can be accessed using the public token, except for “register” and “user delete”, which require the secret token. For more information, please refer to the “Create an app” documentation.

This feature provides flexibility for storing additional user data in your own backend while only sending necessary data to the sentc backend.

In general, every main function in sentc has two equivalent functions with a prepare and done prefix.

use sentc::keys::StdUser;
use sentc::user::done_register;

async fn register()
{
	//normal register
	let user_id = StdUser::register("the-username", "the-password").await.unwrap();
}

fn example()
{
	//no future here
	//call this before you do the request to your backend
	let input = StdUser::prepare_register("identifier", "password").unwrap();
}

fn done(input: &str)
{
	//call this after you call the api. in the client
	let user_id = done_register(input).unwrap();
}

To retrieve the necessary server input for your API, call the prepare function in the client. Once you have this input, make a request to your own backend API using the secret token provided by sentc.

§Response

The response from our api is always structured the same. It is in json format.

Successfully response:

{
	"status": true,
	"result": "<a message or the fetched values>"
}

Failed responses:

{
	"status": false,
	"err_msg": "<text of the error message from the api>",
	"err_code": 0
	//api error code as number
}

The done functions will check every server response like this to get the right result.

§Authentication

For some requests a jwt is needed. Just pass the jwt in Authorization header as Bearer token.

Header name: Authorization
Header value: Bearer <the_jwt>

§App token

For every request, you must send your app token. The sdk will send your public app token automatically. Send it with an x-sentc-app-token header:

Header name: x-sentc-app-token
Header value: <your_app_token>

Use your public token for every frontend related requests and your secret token only for requests from your backend.

§User

The default app settings for user register are from another backend because sentc won’t save other data then the keys and the username.

There is no need for an auth header for registration and login.

§Register

When creating an account, call the prepare function and send the input string to our api to the endpoint with a post request: https://api.sentc.com/api/v1/register

use sentc::keys::StdUser;

fn example()
{
	//no future here
	//call this before you do the request to your backend
	let input = StdUser::prepare_register("identifier", "password").unwrap();
}

After your user registration call this function in the client, to check the response:

use sentc::user::done_register;

fn done(output: &str)
{
	//call this after you call the api. in the client
	let user_id = done_register(output).unwrap();
}

This function will throw an error if the server output is not correct.

Or simply check the status of the json response in your backend.

§Login

Logging in involves multiple requests and data sharing. The recommended approach is to simply log in on the client-side and then call your backend to retrieve additional data. You can verify the JWT token from the user to ensure security.

Alternatively, you can use your own backend’s login process and then log in again to the sentc API on the client-side.

The sentc API login is a highly secure process because the user’s password never leaves their client device.

You can simply check the jwt from the sentc api with your jwt public key see more at “Create an app”.

§Register device

The “Prepare register device” function is similar to the initial user registration process. However, the validation for device registration is the same as described in the “User” section.

To register a device, send the necessary input to our API endpoint using a POST request without a JWT at: https://api.sentc.com/api/v1/user/prepare_register_device

use sentc::keys::StdUser;

fn example()
{
	let input = StdUser::prepare_register_device_start("device_identifier", "device_password").unwrap();
}

To check in the client if the request was correct, use the done function with the server output:

use sentc::user::done_register_device_start;

fn example(server_output: &str)
{
	let input = done_register_device_start(server_output).unwrap();
}

This function will throw an error if the server output is not correct.

§Group

Do not forget to send an Authorization header with the Jwt as Bearer value.

§Create group

To create a group, call the “prepare” function from the user object as we need the user keys.

Send the necessary input to this endpoint using a POST request: https://api.sentc.com/api/v1/group

The input should contain all the client-related values needed to create a group, such as group keys and the encrypted group key by the user’s public key.

Upon successful API request, the resulting group ID will be returned.

use sentc::keys::StdUser;

fn example()
{
	let input = StdUser::prepare_create_group().unwrap();
}

§Delete group

To delete a group call this endpoint with the jwt in header and a delete request: https://api.sentc.com/api/v1/group/<group_id>

§Check group access

At your backend you can also check if a user got access to a group. Use this endpoint: https://api.sentc.com/api/v1/group/<group_id>/light with a GET request.

The response is either an error with status code 310 or a json object:

{
    "group_id",
    "parent_group_id",
    "rank",
    "created_time",
    "joined_time",
    "is_connected_group",
    "access_by"
}

Access by describes how the user access this group. Either direct, as member of a parent group or from a connected group.

§Refreshing the jwt

Like we said in “user - Authentication and JWT” there are three different strategies to handle the refreshing.

§Refresh directly by the sdk

This is the default method. Both the refresh and the jwt are stored in the client. When calling the api and the jwt is invalid this token is used.

In this scenario, a request is made to your endpoint with the old JWT token included in the Authorization header. To refresh the token, make a PUT request to the refresh endpoint on the sentc API from your backend: https://api.sentc.com/api/v1/refresh. Include the old JWT token in the Authorization Bearer header.

§Disable Mfa from server

To disable the Multi-factor auth from your backend for a user call this endpoint: https://api.sentc.com/api/v1/user/forced/disable_otp with a delete request and the following body:

{
	"user_identifier": "<user to disable otp>"
}

§Self-hosting

The sdk connects to an api. The api will store the encrypted user and group keys and will also manage the key rotation and group member.

§Docker

You can choose docker to run the api. To get started, download the sentc/hosting repo. It contains basic docker-compose to get the server running.

git clone https://github.com/sentclose/hosting
cd hosting

Next copy and rename .env.sample to .env and sentc.env.sample to sentc.env

The .env file contains config about the used container. You can change the version of each container.

Optional but recommended: Change the mysql env too.

Next you have to create a root key with the sentc key gen tool and paste it into ROOT_KEY in the sentc.env file.

§Root key generation

You can use the docker image to generate a key:

docker-compose -f key_gen/docker-compose.yml up

Without the -d flag to get the key output. Copy your key, and then you can down the container:

docker-compose -f key_gen/docker-compose.yml down -v

And delete the image

docker image rm sentc/key_gen

§Start

The default is mariadb with redis server.

docker-compose -f mysql/docker-compose.yml up -d

Now everything is running and you can start.

§Non default

If you are using an external Database, or a Database which is running native, then use the mysql/docker-compose.stand_alone.yml file.

Before starting set also the both Env: MYSQL_HOST (your host where the db is running), MYSQL_DB (the database name).

docker-compose -f mysql/docker-compose.external_db.yml up -d

Keep in mind that this will use the array cache as default not redis. If you have redis also running, set the Env CACHE to 2 and the Env REDIS_URL to your running redis url instance.

To use the sqlite container use this compose file:

docker-compose -f sqlite/docker-compose.yml up -d

This will start the sqlite version of the api. Make sure to place in the sqlite database in the folder: db/sqlite. You can get it from the api repo.

§Server

This hosting approach not be directly access from the outside. Use a reverse proxy like nginx to handle tls. Sentc itself will use http.

server {
    client_max_body_size 6m;    # to make sure the file upload works
    
    server_name <your_server_name>
    
    location / {
        proxy_pass http://localhost:3002; # redirect to your running docker container. Sentc uses port 3002 as default.
        proxy_http_version 1.1;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

§SDK change

Set the base_url option in your SDK init to your hosted version to make sure that the sentc backend is not used.

For rust sdk set it everywhere in the code where it is asked for the base_url like StdUser::register(“base_url”,…).

§Register a self-hosted app

You can access your dashboard by going to your address where your instance running. Then simply follow the register an app.

Use your public and secret token from this app.

§Disable app creation

Now the registration is still open for everyone. Set the Env CUSTOMER_REGISTER to 0 in your sentc.env file and restart your docker container. Now none can create a new account and register apps except your account.

docker-compose -f mysql/docker-compose.yml stop

And then start again.

Re-exports§

  • pub use sentc_crypto::sdk_common as crypto_common;

Modules§

Functions§

  • Helper function to get the head and the encrypted data Sentc stores some information about the encryption in front of the encrypted data as a head for decryption. Information about the key and algorithm is used and if it is signed and if so what alg and key was used.
  • The same as split_head_and_encrypted_data but for strings to get just the head back not the string and head.

Type Aliases§

  • The map shows on what index and the key vec the key is in.