Skip to content

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
Terminal window
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.

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>
);
}
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
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
Field Value Description
HEX 'hex' Hexadecimal string output
BASE64 'base64' Base64 string output, with trailing padding, no line wraps
Field Type Default Description
encoding CryptoEncoding CryptoEncoding.HEX Format the digest string is returned in
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.

  • 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, expo/expo#18215, expo/expo#24021, expo/expo#6512.

@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).