Crypto
@symbiote-native/crypto wraps
expo-crypto — cryptographically
secure random bytes, randomUUID, and string/buffer digest hashing — so every SymbioteNative
adapter can reach it. Like 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
Section titled “Installation”pnpm add @symbiote-native/cryptoScaffolding 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.
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).
No platform permission strings are needed for this package — random-byte generation and digest hashing touch no protected device capability.
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.
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> );}<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>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.
<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.
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.
Functions
Section titled “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
Section titled “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
Section titled “CryptoEncoding”| Field | Value | Description |
|---|---|---|
HEX |
'hex' |
Hexadecimal string output |
BASE64 |
'base64' |
Base64 string output, with trailing padding, no line wraps |
ICryptoDigestOptions
Section titled “ICryptoDigestOptions”| Field | Type | Default | Description |
|---|---|---|---|
encoding |
CryptoEncoding |
CryptoEncoding.HEX |
Format the digest string is returned in |
AES-GCM
Section titled “AES-GCM”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.
Common questions
Section titled “Common questions”digestStringAsyncisundefined(“undefined is not an object”). The native module did not load: rebuild the native app after installing and check the autolinking step.randomUUIDreturns 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
undefinedunder Jest. The native module is absent in tests; inject a fake.
Sources: Expo Crypto guide, expo/expo#18215, expo/expo#24021, expo/expo#6512.
How the wrapper works
Section titled “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’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).