# Crypto

> expo-crypto wrapped for every SymbioteNative adapter — cryptographically secure random bytes, UUIDs, and digest hashing.

`@symbiote-native/crypto` wraps
[`expo-crypto`](https://github.com/expo/expo/tree/main/packages/expo-crypto) — cryptographically
secure random bytes, `randomUUID`, and string/buffer digest hashing — so every SymbioteNative
adapter can reach it. Like [local auth](/docs/packages/local-auth/), every function here is a
plain sync/async call with no per-instance state or event stream — no hook/composable/service to
reach for. Upstream's AES-GCM surface (its `aes/` subfolder) is ported too: `AESEncryptionKey`,
`AESSealedData`, `aesEncryptAsync` and `aesDecryptAsync`, plain classes and functions on every
adapter.

| OS platform | Support |
| ----------- | ------- |
| iOS         | ✅ live |
| Android     | ✅ live |

| Framework adapter | Support |
| ----------------- | ------- |
| React             | ✅ live |
| Vue               | ✅ live |
| Angular           | ✅ live |
| Svelte            | ✅ live |
| Solid             | ✅ live |

## Installation

```sh
pnpm add @symbiote-native/crypto
```

Scaffolding or extending a SymbioteNative app? `npx @symbiote-native/cli new --crypto` (or
`add --crypto` in an existing app) installs and wires this for you — see
[`@symbiote-native/cli`](https://github.com/OneEyed1366/symbiote-native/tree/master/packages/cli).

`expo-crypto` and `expo-modules-core` come along as regular dependencies, pinned to exact
versions — never install either yourself, and never add the `expo` meta-package to your project
(it bundles its own Metro/Babel pipeline, which conflicts with this project's own).

<Aside type="danger" title="Native setup is required before first use">
  `expo-crypto`'s native code is discovered by `expo-modules-autolinking`, a
  different mechanism from the `react-native.config.cjs`/podspec autolinking
  every other SymbioteNative wrapper uses — and it isn't the standard Expo setup
  flow either, since this project never installs the `expo` meta-package. Follow
  [How to: wire up an Expo native
  module](/docs/howtos/expo-native-module-setup/) once per app first; it covers
  this package and every future `expo-modules-core` package with zero further
  native changes.
</Aside>

No platform permission strings are needed for this package — random-byte generation and digest
hashing touch no protected device capability.

## Usage

All five adapters — React, Vue, Angular, Svelte, Solid — re-export the exact same free functions;
there is no per-adapter hook/composable/service to reach for, since nothing here holds live state
or a subscription.

<Tabs syncKey="framework">
  <TabItem label="React">
    ```tsx
    import { useEffect, useState } from 'react';
    import {
      CryptoDigestAlgorithm,
      digestStringAsync,
      randomUUID,
    } from '@symbiote-native/crypto/react';

    export default function CryptoDemo() {
      const [uuid, setUuid] = useState('');
      const [hash, setHash] = useState('');

      useEffect(() => {
        setUuid(randomUUID());
      }, []);

      const handleHash = () => {
        digestStringAsync(CryptoDigestAlgorithm.SHA256, 'Confirm it is you').then(setHash);
      };

      return (
        <view>
          <text>UUID: {uuid}</text>
          <pressable onPress={handleHash}>
            <text>Hash a string</text>
          </pressable>
          {hash && <text>SHA-256: {hash}</text>}
        </view>
      );
    }
    ```

  </TabItem>
  <TabItem label="Vue">
    ```vue
    <script setup lang="ts">
    import { onMounted, ref } from 'vue';
    import {
      CryptoDigestAlgorithm,
      digestStringAsync,
      randomUUID,
    } from '@symbiote-native/crypto/vue';

    const uuid = ref('');
    const hash = ref('');

    onMounted(() => {
      uuid.value = randomUUID();
    });

    function handleHash(): void {
      void digestStringAsync(CryptoDigestAlgorithm.SHA256, 'Confirm it is you').then(value => {
        hash.value = value;
      });
    }
    </script>

    <template>
      <view>
        <text>UUID: {{ uuid }}</text>
        <pressable @press="handleHash">
          <text>Hash a string</text>
        </pressable>
        <text v-if="hash">SHA-256: {{ hash }}</text>
      </view>
    </template>
    ```

  </TabItem>
  <TabItem label="Angular">
    ```ts
    import { Component, signal } from '@angular/core';
    import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';
    import {
      CryptoDigestAlgorithm,
      digestStringAsync,
      randomUUID,
    } from '@symbiote-native/crypto/angular';

    @Component({
      standalone: true,
      imports: [SYMBIOTE_ELEMENTS],
      template: `
        <view>
          <text>UUID: {{ uuid() }}</text>
          <pressable (press)="handleHash()">
            <text>Hash a string</text>
          </pressable>
          @if (hash()) {
            <text>SHA-256: {{ hash() }}</text>
          }
        </view>
      `,
    })
    export class CryptoDemo {
      readonly uuid = signal(randomUUID());
      readonly hash = signal('');

      handleHash(): void {
        digestStringAsync(CryptoDigestAlgorithm.SHA256, 'Confirm it is you').then(value =>
          this.hash.set(value),
        );
      }
    }
    ```

    There's no per-instance service to `inject()` here — every function is a plain free function
    off the core package.

  </TabItem>
  <TabItem label="Svelte">
    ```svelte
    <script lang="ts">
      import {
        CryptoDigestAlgorithm,
        digestStringAsync,
        randomUUID,
      } from '@symbiote-native/crypto/svelte';

      let uuid = $state('');
      let hash = $state('');

      $effect(() => {
        uuid = randomUUID();
      });

      function handleHash(): void {
        digestStringAsync(CryptoDigestAlgorithm.SHA256, 'Confirm it is you').then(value => {
          hash = value;
        });
      }
    </script>

    <view>
      <text>UUID: {uuid}</text>
      <pressable onPress={handleHash}>
        <text>Hash a string</text>
      </pressable>
      {#if hash}
        <text>SHA-256: {hash}</text>
      {/if}
    </view>
    ```

    There's no per-instance rune to reach for here either — every function is a plain free
    function off the core package.

  </TabItem>
  <TabItem label="Solid">
    ```tsx
    import { createSignal, onMount } from 'solid-js';
    import {
      CryptoDigestAlgorithm,
      digestStringAsync,
      randomUUID,
    } from '@symbiote-native/crypto/solid';

    export default function CryptoDemo() {
      const [uuid, setUuid] = createSignal('');
      const [hash, setHash] = createSignal('');

      onMount(() => {
        setUuid(randomUUID());
      });

      function handleHash(): void {
        digestStringAsync(CryptoDigestAlgorithm.SHA256, 'Confirm it is you').then(setHash);
      }

      return (
        <view>
          <text>UUID: {uuid()}</text>
          <pressable onPress={handleHash}>
            <text>Hash a string</text>
          </pressable>
          {hash() && <text>SHA-256: {hash()}</text>}
        </view>
      );
    }
    ```

    There's no per-instance primitive to reach for here either - every function is a plain free
    function off the core package, read straight into a signal.

  </TabItem>
</Tabs>

## API

### Functions

| Signature                                                                                                             | Description                                                                                               |
| --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `getRandomBytes(byteCount: number): Uint8Array`                                                                       | Synchronously generates `byteCount` cryptographically secure random bytes. `byteCount` must be `0`-`1024` |
| `getRandomBytesAsync(byteCount: number): Promise<Uint8Array>`                                                         | Same as `getRandomBytes`, asynchronously                                                                  |
| `getRandomValues<T extends ITypedArray>(typedArray: T): T`                                                            | Fills a provided integer-based `TypedArray` with cryptographically secure random values, in place         |
| `randomUUID(): string`                                                                                                | A V4 UUID (RFC4122), generated using cryptographically secure random values                               |
| `digestStringAsync(algorithm: CryptoDigestAlgorithm, data: string, options?: ICryptoDigestOptions): Promise<IDigest>` | Generates a digest of a string, formatted per `options.encoding` (defaults to `CryptoEncoding.HEX`)       |
| `digest(algorithm: CryptoDigestAlgorithm, data: BufferSource): Promise<ArrayBuffer>`                                  | Generates a digest of raw bytes, returned as an `ArrayBuffer`                                             |

### `CryptoDigestAlgorithm`

| Field    | Value       | Description                   |
| -------- | ----------- | ----------------------------- |
| `SHA1`   | `'SHA-1'`   | 160 bits                      |
| `SHA256` | `'SHA-256'` | 256 bits, collision-resistant |
| `SHA384` | `'SHA-384'` | 384 bits, collision-resistant |
| `SHA512` | `'SHA-512'` | 512 bits, collision-resistant |
| `MD2`    | `'MD2'`     | 128 bits. `@platform ios`     |
| `MD4`    | `'MD4'`     | 128 bits. `@platform ios`     |
| `MD5`    | `'MD5'`     | 128 bits                      |

### `CryptoEncoding`

| Field    | Value      | Description                                                |
| -------- | ---------- | ---------------------------------------------------------- |
| `HEX`    | `'hex'`    | Hexadecimal string output                                  |
| `BASE64` | `'base64'` | Base64 string output, with trailing padding, no line wraps |

### `ICryptoDigestOptions`

| Field      | Type             | Default              | Description                             |
| ---------- | ---------------- | -------------------- | --------------------------------------- |
| `encoding` | `CryptoEncoding` | `CryptoEncoding.HEX` | Format the digest string is returned in |

### AES-GCM

```ts
import {
  AESEncryptionKey,
  AESKeySize,
  aesDecryptAsync,
  aesEncryptAsync,
} from '@symbiote-native/crypto';

const key = await AESEncryptionKey.generate(AESKeySize.AES256);
const sealed = await aesEncryptAsync(plaintextBase64, key);
const plain = await aesDecryptAsync(sealed, key, { output: 'base64' });
```

| Symbol                                          | Description                                                                              |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `AESEncryptionKey`                              | `generate(size?)`, `import(bytes)`, `import(string, 'hex' \| 'base64')`, `bytes()`, `encoded()` |
| `AESSealedData`                                 | `fromParts(iv, ciphertext, tag \| tagLength?)`, `fromCombined(combined, config?)`, `iv()`, `tag()`, `ciphertext()`, `combined()` |
| `aesEncryptAsync(plaintext, key, options?)`     | `options`: `nonce` (`{ length }` or `{ bytes }`), `tagLength`, `additionalData`          |
| `aesDecryptAsync(sealedData, key, options?)`    | `options`: `output` (`'bytes'` or `'base64'`), `additionalData`                          |

A string input is base64. `tagLength` is ignored on Apple, where the tag is always 16 bytes, and
`AESKeySize.AES192` is unsupported on web.

## Notes

<Aside type="note" title="Input validation mirrors upstream exactly">
  `getRandomBytes`/`getRandomBytesAsync` validate `byteCount` is a number in
  `0`-`1024` (inclusive), throwing a plain `TypeError` otherwise, and floor a
  fractional count. `digestStringAsync` validates
  `algorithm`/`data`/`options.encoding` and throws `CryptoError` (a `TypeError`
  subclass, `code: 'ERR_CRYPTO'`) on an invalid value. `digest` prefers a native
  `digestAsync` when present; otherwise it allocates a fixed-size output buffer
  (sized via a per-algorithm length lookup table) and calls the native sync
  `digest`. This port skips upstream's `__DEV__`/remote-debugger `Math.random`
  fallback — a React Native debugging-tool concern, not applicable to this
  package's native-call path.
</Aside>

## Common questions

- **`digestStringAsync` is `undefined` ("undefined is not an object").** The native module did not
  load: rebuild the native app after installing and check the autolinking step.
- **`randomUUID` returns a promise.** Reported intermittently after a native binary update; restart
  the app and rebuild.
- **Which hash algorithms?** SHA-256 and SHA-512 are the safe choices; SHA-1 is available but not
  recommended for anything security-sensitive. Encodings are hex and base64.
- **Digest returns `undefined` under Jest.** The native module is absent in tests; inject a fake.

Sources: [Expo Crypto guide](https://codingeasypeasy.com/blog/expo-crypto-securely-implement-cryptographic-functions-in-your-react-native-apps/),
[expo/expo#18215](https://github.com/expo/expo/issues/18215),
[expo/expo#24021](https://github.com/expo/expo/issues/24021),
[expo/expo#6512](https://github.com/expo/expo/issues/6512).

## How the wrapper works

`@symbiote-native/crypto` ships zero React/Vue/Angular/Svelte/Solid logic in `expo-crypto` itself — its
functions, enums, and the `CryptoError` validation class are hand-ported, verbatim, into this
package's own `core/`, resolving the native module through `expo-modules-core`'s
`requireNativeModule` rather than the `expo` meta-package this project never installs:

```
packages/crypto/src/
├── core/     # framework-agnostic: every function above, CryptoDigestAlgorithm, CryptoEncoding,
│             # the option/result types, and CryptoError; native-module.ts resolves the native
│             # module via expo-modules-core's requireNativeModule
├── react/    # @symbiote-native/crypto/react   — export * from '../core'
├── vue/      # @symbiote-native/crypto/vue     — export * from '../core'
├── svelte/   # @symbiote-native/crypto/svelte  — export * from '../core'
├── solid/    # @symbiote-native/crypto/solid   — export * from '../core'
└── angular/  # @symbiote-native/crypto/angular — export * from '../core'
```

Same shape as [local auth](/docs/packages/local-auth/)'s five adapter entries: single-file
re-exports with no lifecycle code at all, since every function here is stateless and one-shot —
there is nothing for a hook, composable, or service to subscribe to or clean up. The native code
itself is never vendored or copied — `expo-modules-autolinking` resolves it straight out of
`node_modules` (see [the native setup guide](/docs/howtos/expo-native-module-setup/)).
