Cryptography
User & Device Authentication

User & Device Authentication

Cryptographic Dependencies and actual implementation

  • opaque_login: OPAQUE login flow
  • userChainAddDevice: see user-chain
  • kdf: sodium.crypto_kdf_hkdf_sha256_expand(sodium.crypto_kdf_hkdf_sha256_extract(key, subkeyId), context, crypto_aead_xchacha20poly1305_ietf_KEYBYTES)
  • signingKeyPairGen: sodium.crypto_sign_keypair()
  • encryptionKeyPairGen: sodium.crypto_box_keypair()
  • sign: sodium.crypto_sign_detached(message, privateKey)
  • noncegen: sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES)
  • secretboxKeygen: sodium.crypto_secretbox_keygen()
  • encrypt: sodium.crypto_secretbox_easy(message, nonce, key)
  • decrypt: sodium.crypto_secretbox_opne_easy(ciphertext, nonce, key)

Login

Using the OPAQUE protocol to authenticate the client with the server the MainDevice can be recovered. With the MainDevice the client automatically add the current client as new device to the user-chain. All workspace keys are decrypted and in case of a long-term session the workspace key boxes for the new device are created.

exportKey, sessionKey = opaque_login()
encryptionKey = kdf(1111, "m_device", exportKey);
mainDevice = decrypt(
  ciphertext,
  nonce,
  encryptionKey
);
{
  signingPublicKey,
  signingPrivateKey,
  encryptionPublicKey,
  encryptionPrivateKey,
  encryptionPublicKeySignature,
  createdAt,
} = mainDevice;
// create device
deviceSigningKeyPair = signingKeyPairGen();
deviceEncryptionKeyPair = encryptionKeyPairGen();
deviceEncryptionPublicKeySignature = sign(
  concat("user_device_encryption_public_key", deviceEncryptionKeyPair.publicKey),
  deviceSigningKeyPair.privateKey
);
expiredAt = determineExpiredAt(); // undefined if permanent device (mobile), 30 days for long web session, 24h for short web session
userChainAddDeviceEvent = userChainAddDevice(mainDevice, deviceSigningKeyPair.publicKey, deviceEncryptionKeyPair.publicKey, deviceEncryptionPublicKeySignature, expiredAt);
sessionTokenSignature = sign(concat("login_session_key", sessionKey), deviceSigningKeyPair.privateKey);
// additional steps only done for web devices
webDeviceKey = secretboxKeygen();
webDeviceNonce = noncegen();
webDeviceCiphertext = encrypt(
  {
    deviceSigningKeyPair.publicKey,
    deviceSigningKeyPair.privateKey,
    deviceEncryptionKeyPair.publicKey,
    deviceEncryptionKeyPair.privateKey,
    deviceEncryptionPublicKeySignature,
  },
  webDeviceNonce,
  webDeviceKey
);

opaque_login https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/login/LoginForm.tsx#L70-L75 (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/authentication/loginHelper.ts#L65-L76 (opens in a new tab)

load the latest user chain and decryptMainDevice https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/authentication/loginHelper.ts#L78-L87 (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/packages/common/src/decryptMainDevice/decryptMainDevice.ts (opens in a new tab)

UserChain.addDevice https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/authentication/loginHelper.ts#L124-L139 (opens in a new tab)

sign sessionToken to verify the client has access to the private key of the new device (to prevent a MITM attack replacing the device) https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/authentication/loginHelper.ts#L141-L146 (opens in a new tab)

After the device was added to the user, the client starts adding the device to known workspaces. This is triggered here:

https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/login/LoginForm.tsx#L95-L97 (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/workspace/attachDeviceToWorkspaces.ts (opens in a new tab)

The exact process is documented in the section Workspace Encryption und Adding a new device to the workspace and creating the corresponding encryption box on a login.

Data stored

On mobile and desktop with access to a secure storage the sessionKey and device private keys are stored in the secure storage.

On web devices without access to a secure storage the sessionKey, webDeviceKey and webAccessToken is stored in localstorage.

The mainDevice is only stored in memory regardless of the environment.

More details on storage can be found in Secure Client Storage.

Recovering a web device

When a user opens the browser and still has a valid (non-expired and non-revoked session) then the client uses the stored webAccessToken to retrieve the encrypted webDevice (ciphertext & nonce) from the backend. The client then decrypts the webDevice and stores the decrypted webDevice in memory.

webDevice = decrypt(webDeviceCiphertext, webDeviceNonce, webDeviceKey);

https://github.com/serenity-kit/Serenity/blob/main/apps/app/store/webDeviceStore.ts#L31-L44 (opens in a new tab)

Active Session Authentication

An authorization token is constructed for every request.

sessionToken = kdf("AQEBAQEBAQEBAQEBAQEBAQ", "session_token", sessionKey);
datetime = generateDatetimeAsIsoString()
sessionDatetimeSubkey = kdf(datetime, "session_datetime", sessionKey);
authorizationToken = `${sessionToken}|${datetime}|${sessionDatetimeSubkey}`;

The authorizationToken is sent along as Authorization header. Based on that the backend can identify the user and verify the session.

Client code: https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/urqlClient/urqlClient.ts#L76-L81 (opens in a new tab) Server code: https://github.com/serenity-kit/Serenity/blob/main/apps/backend/src/createServer.ts#L39-L70 (opens in a new tab)

More details in Client Server (Authentication & Authorization).

Opaque Server Public Key Verification

In order to make sure the user is talking to the correct opaque server the server's public key is verified.

https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/authentication/loginHelper.ts#L74-L76 (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/getOpaqueServerPublicKey/getOpaqueServerPublicKey.ts (opens in a new tab)

In addition also used when retrieving the main device in the password verification.

https://github.com/serenity-kit/Serenity/blob/main/apps/app/components/verifyPasswordModal/VerifyPasswordModal.tsx (opens in a new tab)