From 7ac93a95e6c4a58bd19d385856b893ac72edddd9 Mon Sep 17 00:00:00 2001 From: byGalax Date: Wed, 13 May 2026 23:55:07 +0200 Subject: [PATCH] feat(mobile): crypto + secret-store + session-storage adapters --- apps/mobile/lib/cryptoBackend.ts | 28 ++++++++++++++++++++++++++++ apps/mobile/lib/secretStore.ts | 20 ++++++++++++++++++++ apps/mobile/lib/sessionStorage.ts | 7 +++++++ 3 files changed, 55 insertions(+) create mode 100644 apps/mobile/lib/cryptoBackend.ts create mode 100644 apps/mobile/lib/secretStore.ts create mode 100644 apps/mobile/lib/sessionStorage.ts diff --git a/apps/mobile/lib/cryptoBackend.ts b/apps/mobile/lib/cryptoBackend.ts new file mode 100644 index 0000000..0e6b6a8 --- /dev/null +++ b/apps/mobile/lib/cryptoBackend.ts @@ -0,0 +1,28 @@ +import * as s from 'react-native-libsodium'; +import type { CryptoBackend } from '@chat-app/shared/crypto'; + +// react-native-libsodium re-exports libsodium-wrappers' API shape, so +// this adapter is the synchronous twin of the desktop one +// (`apps/desktop/src/lib/cryptoBackend.ts`). No WASM warm-up gate is +// needed — the native module is ready as soon as the module loads. +export function createLibsodiumBackend(): CryptoBackend { + return { + name: 'react-native-libsodium', + nonceLength: s.crypto_box_NONCEBYTES, + publicKeyLength: s.crypto_box_PUBLICKEYBYTES, + privateKeyLength: s.crypto_box_SECRETKEYBYTES, + secretboxKeyLength: s.crypto_secretbox_KEYBYTES, + secretboxNonceLength: s.crypto_secretbox_NONCEBYTES, + randomBytes: (n) => s.randombytes_buf(n), + generateKeyPair: () => { + const kp = s.crypto_box_keypair(); + return { publicKey: kp.publicKey, privateKey: kp.privateKey }; + }, + box: (plaintext, nonce, recipientPublicKey, senderPrivateKey) => + s.crypto_box_easy(plaintext, nonce, recipientPublicKey, senderPrivateKey), + boxOpen: (ciphertext, nonce, senderPublicKey, recipientPrivateKey) => + s.crypto_box_open_easy(ciphertext, nonce, senderPublicKey, recipientPrivateKey), + secretbox: (plaintext, nonce, key) => s.crypto_secretbox_easy(plaintext, nonce, key), + secretboxOpen: (ciphertext, nonce, key) => s.crypto_secretbox_open_easy(ciphertext, nonce, key), + }; +} diff --git a/apps/mobile/lib/secretStore.ts b/apps/mobile/lib/secretStore.ts new file mode 100644 index 0000000..85c3119 --- /dev/null +++ b/apps/mobile/lib/secretStore.ts @@ -0,0 +1,20 @@ +import { Buffer } from 'buffer'; +import * as SecureStore from 'expo-secure-store'; +import type { SecretStore } from '@chat-app/shared/auth'; + +// SecretStore contract uses Uint8Array values; SecureStore only takes +// strings, so we base64 at the boundary. iOS Keychain max value size +// is generous (a few MB); private keys are 32 bytes so we are well +// within limits. +export const secretStore: SecretStore = { + async getSecret(key) { + const v = await SecureStore.getItemAsync(key); + return v ? new Uint8Array(Buffer.from(v, 'base64')) : null; + }, + async setSecret(key, value) { + await SecureStore.setItemAsync(key, Buffer.from(value).toString('base64')); + }, + async removeSecret(key) { + await SecureStore.deleteItemAsync(key); + }, +}; diff --git a/apps/mobile/lib/sessionStorage.ts b/apps/mobile/lib/sessionStorage.ts new file mode 100644 index 0000000..347edeb --- /dev/null +++ b/apps/mobile/lib/sessionStorage.ts @@ -0,0 +1,7 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; + +// supabase-js v2 accepts any object with async getItem / setItem / +// removeItem returning Promise / Promise. RN's +// AsyncStorage matches that shape for shape; we just re-export it +// under a name that signals intent at the call site. +export const sessionStorage = AsyncStorage;