Cryptography
Secure Client Storage

Secure Client Storage

The following information should be stored locally if possible:

  • device private keys (signing and encryption)
  • sessionKey (OPAQUE)
  • unencrypted content (folder names, document titles, document names)
  • chain events (workspace-chains, user-chains, document-chains)

Storage for content

General

Sqlite was chosen as the database to store the data locally. Depending on the environment the database is only stored in-memory and/or encrypted and persisted on the local file system.

  • Web: only in-memory
  • Electron: in-memory and regularliy and encrypted on the local file system
  • iOS and Android: every change is directly encrypted and persisted on the local file system using sqlcipher

All code related to the SQLite database integration can be found in this folder https://github.com/serenity-kit/Serenity/tree/main/apps/app/store (opens in a new tab).

The following data is currently stored in there:

The benefit of using a local sqlite DB as state management is, that it the same way in all environments and only need to switch the storage implementation. This simplify the application logic and ideally reduces bugs.

And since chain and workspaceMemberDevicesProof entries are never overwritten by new data coming from the server, the server is discourage to send broken entries since a client either can persist then or could be long-living if users keep their browser tabs open.

Web

For the web client the in-memory Sqlite DB is used. The database is created on app start and deleted on logout.

https://github.com/serenity-kit/Serenity/blob/main/apps/app/store/sql/sql.web.ts#L6-L21 (opens in a new tab) (open DB)

https://github.com/serenity-kit/Serenity/blob/main/apps/app/store/sql/sql.web.ts#L23-L26 (opens in a new tab) (reset DB on logout)

Electron

For the Electron client the in-memory Sqlite DB is used.

The in-memory Sqlite db is regularliy exported (currently debounces on every document write), encrypted and stored on the local file system. The key and nonce to encrypt the database is encrypted using https://www.electronjs.org/docs/latest/api/safe-storage (opens in a new tab) which is also stored on the local file system.

key = sodium.randombytes_buf(sodium.crypto_secretbox_KEYBYTES);
nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
encryptedKeyAndNonce = electron.safeStorage.encryptString(serialize(key, nonce));
encryptedDatabase = sodium.crypto_secretbox_easy(database, nonce, key);

https://github.com/serenity-kit/Serenity/blob/main/apps/app/store/sql/sql.electron.ts#L29-L39 (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/apps/desktop-app/src/index.ts#L114-L157 (opens in a new tab)

When the application is started the encrypted database is loaded and decrypted using the key and nonce. The decrypted database is then used as the in-memory database.

decryptedDataString = electron.safeStorage.decryptString(encryptedKeyAndNonce);
decryptedData = deserialize(decryptedDataString);
decryptedDatabase = sodium.crypto_secretbox_open_easy(data, nonce, key);

https://github.com/serenity-kit/Serenity/blob/main/apps/app/store/sql/sql.electron.ts#L9-L23 (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/apps/desktop-app/src/index.ts#L159-L185 (opens in a new tab)

On logout the database, key-and-nonce file is deleted.

https://github.com/serenity-kit/Serenity/blob/main/apps/app/navigation/screens/logoutInProgressScreen/LogoutInProgressScreen.tsx#L139 (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/apps/app/store/sql/sql.electron.ts#L40C1-L42 (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/apps/desktop-app/src/index.ts#L187-L197 (opens in a new tab)

Considerations for alternative designs

An alternative design could be to use https://github.com/signalapp/better-sqlite3 (opens in a new tab). It is a SQLCipher implementation working in Electron. The API is async and won't work for a good UX. Therefor we would need to manage the writes between the in-memory and the persisted DB. This includes managing failed writes and possible recovery. This is out of scope for now, but we will consider it in the future.

Mobile

For the mobile devices (ReactNative) no in-memory Sqlite database is used, but instead a React Native wrapper library https://github.com/OP-Engineering/op-sqlcipher (opens in a new tab) to directly use https://github.com/sqlcipher/sqlcipher (opens in a new tab) to persist data encrypted on the device.

Sqlcipher requires and encryption key. The key is generated every time and no key is stored in the secure store of the device. The key is generated using sodium.randombytes_buf(sodium.crypto_secretbox_KEYBYTES).

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

When the user logs out a the database is deleted and the key in the secure store is deleted. Right after that the ready function runs again and creates a new database and key.

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

Database reset on logout

On logout the local in-memory DB must be wiped. For the in-memory DB it is fine to just reconnect and the new instance will be empty.

https://github.com/serenity-kit/Serenity/blob/main/apps/app/navigation/screens/logoutInProgressScreen/LogoutInProgressScreen.tsx#L159 (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/apps/app/store/sql/sql.web.ts#L23-L26 (opens in a new tab)

Storage for device and session keys

Environments

iOS and Android

The expo-secure-store package is used to store a Device using Keychain on iOS and KeyStore on Android. Both leverage a Secure Element to keep the data secure.

Code reference: https://github.com/serenity-kit/Serenity/blob/main/apps/app/store/deviceStore/deviceStore.ts (opens in a new tab)

Also for the sessionKey the expo-secure-store package is used to store the sessionKey in the Keychain on iOS and KeyStore on Android.

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

Desktop clients

To store the device with it's private keys we use Electron's https://www.electronjs.org/docs/latest/api/safe-storage (opens in a new tab). It integrates with the macOS Keychain and Windows Credential Manager on Windows.

Here the safeStorage.encryptString and safeStorage.decryptString is used to encrypt/decrypt a string with the serialized device. The encrypted string is stored on the local file system.

Encryption:

serializedDevice = JSON.stringify(device);
const encryptedDevice = await safeStorage.encryptString(serializedDevice);

Decryption:

const serializedDevice = await safeStorage.decryptString(encryptedDevice);
const device = JSON.parse(serializedDevice);

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

https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/setupElectronInterface/electronInterface.electron.ts#L22-L34 (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/apps/desktop-app/src/index.ts#L17 (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/apps/desktop-app/src/index.ts#L235-L265 (opens in a new tab)

The session key is stored in the same way as the device.

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

Web client

Since in a web client no secure storage available and therefor no content is persistet on the client. Possibly hashes of the chains (e.g. workspace-chain) could be stored, but this is not implemented and no decision in favor of it has been made.

Also device private keys are not stored locally. The only data that is stored locally is the sessionKey, webDeviceAccessToken and a webDeviceKey. The webDeviceKey is used to encrypt the web device (incl. private signing and encryption key) and stored on the server when adding the device to the account.

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

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

When a user open the application the webDeviceAccessToken is used to fetch the encrypted device and decrypt it with the webDeviceKey. The decrypted device is then stored only in memory and will be gone after closing the browser tab.

https://github.com/serenity-kit/Serenity/blob/main/apps/app/hooks/useCachedResources.ts (opens in a new tab) (invoked on app start)

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

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

This design was chosen to ensure that a revoked or expired web device can't be extracted from a compromised browser since the server rejects returning the device ciphertext and nonce. The design that was rejected was to store the device private keys directly in the browser's LocalStorage. This would have been possible, but would have meant that a compromised browser could have extracted the device private keys.

Code reference for the webDeviceStore: https://github.com/serenity-kit/Serenity/blob/main/apps/app/store/webDeviceStore.ts (opens in a new tab)

The session key in comparision is directly stored in localstorage using the @react-native-async-storage/async-storage package: https://github.com/serenity-kit/Serenity/blob/main/apps/app/store/sessionKeyStore/sessionKeyStore.ts (opens in a new tab)

Deleting keys on logout

On logout the device and session keys must be deleted. This is done by calling the clearDeviceAndSessionStorage function.

https://github.com/serenity-kit/Serenity/blob/main/apps/app/navigation/screens/logoutInProgressScreen/LogoutInProgressScreen.tsx#L140 (opens in a new tab)

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