diff --git a/README.md b/README.md index 8e1207f31..c3d58d086 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ yarn add @segment/analytics-react-native @segment/sovran-react-native react-nati npm install --save @segment/analytics-react-native @segment/sovran-react-native react-native-get-random-values @react-native-async-storage/async-storage ``` -_Note: `@react-native-async-storage/async-storage` is an optional dependency. If you wish to use your own persistence layer you can use the `storePersistor` option when initializing the client. Make sure you always have a persistor (either by having AsyncStorage package installed or by explicitly passing a value), else you might get unexpected side-effects like multiple 'Application Installed' events. Read more [Client Options](#client-options)_ +_Note: `@react-native-async-storage/async-storage` is an optional dependency. If you wish to use your own persistence layer you can use the `storePersistor` option when initializing the client. Make sure you always have a persistor (either by having AsyncStorage package installed or by explicitly passing a value), else you might get unexpected side-effects like multiple 'Application Installed' events. Read more [Client Options](#client-options). ⚠️ AsyncStorage is unencrypted—see [Data Storage & Security](#data-storage--security) before storing sensitive PII._ For iOS, install native modules with: @@ -98,7 +98,7 @@ You must pass at least the `writeKey`. Additional configuration options are list | `trackDeepLinks` | false | Enable automatic tracking for when the user opens the app via a deep link (Note: Requires additional setup on iOS, [see instructions](#ios-deep-link-tracking-setup)). | | `defaultSettings` | undefined | Settings that will be used if the request to get the settings from Segment fails. Type: [SegmentAPISettings](https://github.com/segmentio/analytics-react-native/blob/c0a5895c0c57375f18dd20e492b7d984393b7bc4/packages/core/src/types.ts#L293-L299) | | `autoAddSegmentDestination` | true | Set to false to skip adding the SegmentDestination plugin. | -| `storePersistor` | undefined | A custom persistor for the store that `analytics-react-native` leverages. Must match [`Persistor`](https://github.com/segmentio/analytics-react-native/blob/master/packages/sovran/src/persistor/persistor.ts#L1-L18) interface exported from [sovran-react-native](https://github.com/segmentio/analytics-react-native/blob/master/packages/sovran). | +| `storePersistor` | undefined | A custom persistor for the store that `analytics-react-native` leverages. Must match [`Persistor`](https://github.com/segmentio/analytics-react-native/blob/master/packages/sovran/src/persistor/persistor.ts#L1-L18) interface exported from [sovran-react-native](https://github.com/segmentio/analytics-react-native/blob/master/packages/sovran). ⚠️ The default (AsyncStorage) persistor is unencrypted, see [Data Storage & Security](#data-storage--security). | | `proxy` | undefined | `proxy` is a batch URL to post the events. Enable `useSegmentEndpoint` if proxy domain is provided and you want to append the Segment endpoints automatically. If you want to completely customize the proxy by providing a custom URL, disable `useSegmentEndpoint`. Default value is `false`. | | `errorHandler` | undefined | Create custom actions when errors happen, see [Handling errors](#handling-errors). | | `cdnProxy` | undefined | Sets an alternative CDN host for settings retrieval. Enable `useSegmentEndpoint` if `cdnProxy` domain is provided and you want to append the Segment endpoints automatically.

⚠️ **IMPORTANT NOTE:** _Prior to version 2.20.4, any value provided for `cdnProxy` was automatically appended with `/write-key/settings`. **Starting from v2.20.4**, this behavior has **CHANGED**—the SDK will now behave based on the `useSegmentEndpoint` flag._ Please update your configuration accordingly to avoid unexpected issues. ⚠️ | @@ -106,6 +106,33 @@ You must pass at least the `writeKey`. Additional configuration options are list \* The default value of `debug` will be false in production. +### Data Storage & Security + +By default, `analytics-react-native` persists its state—`userId`, `identify` traits, and the queue of events pending upload—to disk using [`@react-native-async-storage/async-storage`](https://github.com/react-native-async-storage/async-storage). **This default persistor stores data as plaintext JSON with no encryption.** On a rooted/jailbroken device, via an ADB backup, or through forensic extraction, that data—including any PII passed to `identify` or `track` calls—can be read. + +If your app handles sensitive PII (email, name, government IDs, etc.), supply your own `storePersistor` backed by encrypted storage, such as the OS Keychain/Keystore via [`react-native-encrypted-storage`](https://github.com/emeraldsanto/react-native-encrypted-storage) or [`react-native-keychain`](https://github.com/oblador/react-native-keychain). Your persistor must implement the [`Persistor`](https://github.com/segmentio/analytics-react-native/blob/master/packages/sovran/src/persistor/persistor.ts#L1-L18) interface: + +```ts +import EncryptedStorage from 'react-native-encrypted-storage'; +import type { Persistor } from '@segment/sovran-react-native'; +import { createClient } from '@segment/analytics-react-native'; + +const EncryptedStoragePersistor: Persistor = { + get: async (key) => { + const value = await EncryptedStorage.getItem(key); + return value ? JSON.parse(value) : undefined; + }, + set: async (key, state) => { + await EncryptedStorage.setItem(key, JSON.stringify(state)); + }, +}; + +const segmentClient = createClient({ + writeKey: 'SEGMENT_API_KEY', + storePersistor: EncryptedStoragePersistor, +}); +``` + ### iOS Deep Link Tracking Setup _Note: This is only required for iOS if you are using the `trackDeepLinks` option. Android does not require any additional setup_ diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 4cbd6b5ba..c440a9811 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -148,6 +148,13 @@ export type Config = { defaultSettings?: SegmentAPISettings; autoAddSegmentDestination?: boolean; collectDeviceId?: boolean; + /** + * Custom persistor for the store. When omitted, the SDK defaults to + * AsyncStorage, which persists state (userId, `identify` traits, and the + * pending event queue) as plaintext JSON with no encryption. Apps handling + * sensitive PII should supply a `storePersistor` backed by encrypted + * storage (e.g. Keychain/Keystore via `react-native-encrypted-storage`). + */ storePersistor?: Persistor; storePersistorSaveDelay?: number; proxy?: string; diff --git a/packages/sovran/src/persistor/async-storage-persistor.ts b/packages/sovran/src/persistor/async-storage-persistor.ts index 0ec8b7934..63db995b9 100644 --- a/packages/sovran/src/persistor/async-storage-persistor.ts +++ b/packages/sovran/src/persistor/async-storage-persistor.ts @@ -31,7 +31,14 @@ function warnIfMissingPackage() { } /** - * Persistor implementation using AsyncStorage + * Persistor implementation using AsyncStorage. + * + * ⚠️ AsyncStorage is unencrypted: state is written as plaintext JSON and is + * readable on rooted/jailbroken devices, via ADB backup, or forensic + * extraction. This includes userId, `identify` traits (email/name/etc.), and + * the pending event queue. Apps handling sensitive PII should pass their own + * `storePersistor` backed by encrypted storage (Keychain/Keystore, e.g. via + * `react-native-encrypted-storage`) instead of relying on this default. */ export const AsyncStoragePersistor: Persistor = { get: async (key: string): Promise => {