One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Crypto

Secure random values, digests, and AES-GCM encryption

Hermes ships no WebCrypto, so One installs the standard crypto globals on native at startup: getRandomValues, randomUUID (RFC 4122 v4), and subtle.digest for SHA-1, SHA-256, SHA-384, and SHA-512. Only missing pieces are installed: a runtime that already implements them keeps its own, and a browser’s crypto is never replaced.

Random bytes come from the platform CSPRNG (arc4random_buf) through a synchronous C++ hybrid object; digests hash with CommonCrypto on iOS and java.security.MessageDigest on Android, on the Nitro thread pool.

One.Crypto adds Expo Crypto’s API on top of the same engine: digest and digestString (SHA-1, SHA-256, SHA-384, SHA-512, and MD5, in hex or base64), plus AES-GCM through AESEncryptionKey, AESSealedData, aesEncrypt, and aesDecrypt. AES runs through javax.crypto on Android and CommonCrypto’s AES on iOS; on the web both use SubtleCrypto, where MD5 is unavailable.

Usage

const id = crypto.randomUUID()
const bytes = crypto.getRandomValues(new Uint8Array(16))
const hash = await crypto.subtle.digest('SHA-256', bytes)

The calls are the web APIs with web semantics: random values fill your array in place, UUIDs come back as strings, and digests resolve an ArrayBuffer.

import { AESEncryptionKey, AESSealedData, One } from 'one'
const hash = await One.Crypto.digestString('SHA-256', 'hello')
const key = await AESEncryptionKey.generate()
const sealed = await One.Crypto.aesEncrypt(new Uint8Array([1, 2, 3]), key)
const stored = await sealed.combined('base64')
const opened = await One.Crypto.aesDecrypt(AESSealedData.fromCombined(stored), key)

From Expo, the renames are mechanical: digestStringAsync is digestString, aesEncryptAsync is aesEncrypt, aesDecryptAsync is aesDecrypt, and the CryptoDigestAlgorithm and CryptoEncoding values are the same strings ('SHA-256', 'hex'). MD2 and MD4 are not offered. On Apple platforms the GCM tag is always 16 bytes when sealing, as in Expo; Android and web honor tagLength. A sealed value from Expo opens here and the reverse, because the combined layout (IV, ciphertext, tag) and defaults are the same.

How it works

getRandomValues writes secure bytes over your own buffer in place, allocating and copying nothing. It accepts integer typed arrays only, rejects shared memory, and throws past 65,536 bytes per call, matching the platform limits. randomUUID is a single native call that formats 16 random bytes as v4. There is no fallback to Math.random: a missing native module throws instead of returning weak bytes, and a binary without the module installs nothing so the error stays clear.

crypto.subtle.digest snapshots the input range synchronously, then hashes off the JavaScript thread and resolves the hash buffer. It takes an ArrayBuffer or a view plus a SHA name or { name } object; detached buffers and non-SHA algorithms reject with the standard error names.

API reference

Digests and encryption

One.Crypto is available from import { One } from 'one'. The AES key and sealed-data classes are named imports from one.

MethodReturns
One.Crypto.digest(algorithm, data)Promise<ArrayBuffer>
One.Crypto.digestString(algorithm, text, options?)Promise<string>
One.Crypto.aesEncrypt(plaintext, key, options?)Promise<AESSealedData>
One.Crypto.aesDecrypt(sealedData, key, options?)Promise<Uint8Array>; Promise<string> with output: 'base64'

DigestAlgorithm is 'MD5', 'SHA-1', 'SHA-256', 'SHA-384', or 'SHA-512'. digest takes an ArrayBuffer or a typed-array view and returns raw hash bytes. digestString encodes its text as UTF-8 before hashing.

DigestStringOptionsTypeDescription
encodingDigestEncoding'hex' by default, or 'base64'.
DigestEncoding'hex' by default, or 'base64'.

AES inputs use BinaryInput: a Uint8Array, an ArrayBuffer, or a base64 string. A string is encoded binary data, so encode text before encrypting it.

AESEncryptOptionsTypeDescription
additionalDataBinaryInputAuthenticated data that is verified but not encrypted or stored in the sealed result.
BinaryInputAuthenticated data that is verified but not encrypted or stored in the sealed result.
nonceGCMNonceParamOmit for a fresh random 12-byte IV; pass { length } for another random size or { bytes } for an explicit IV.
GCMNonceParamOmit for a fresh random 12-byte IV; pass { length } for another random size or { bytes } for an explicit IV.
tagLengthnumberTag bytes: 16 by default, or 15, 14, 13, 12, 8, 4. Apple encryption always produces 16.
numberTag bytes: 16 by default, or 15, 14, 13, 12, 8, 4. Apple encryption always produces 16.
AESDecryptOptionsTypeDescription
additionalDataBinaryInputThe same authenticated data passed when encrypting.
BinaryInputThe same authenticated data passed when encrypting.
output'base64' | 'bytes'Plaintext bytes by default, or a base64 string.
'base64' | 'bytes'Plaintext bytes by default, or a base64 string.

AES keys

MethodReturns
AESEncryptionKey.generate(size?)A promise of a new key; size is 128, 192, or 256 bits, default 256
AESEncryptionKey.import(bytes)A promise of a key from a Uint8Array
AESEncryptionKey.import(text, encoding)A promise of a key from 'hex' or 'base64'
key.bytes()A promise of a copy of the raw key bytes
key.encoded(encoding)A promise of the key as 'hex' or 'base64'

key.size is its size in bits. Exported key bytes are secrets; keep them in SecureStore or another protected location if the key must survive a restart. The combined encrypted payload does not contain the key.

Sealed data

Method or propertyResult
AESSealedData.fromCombined(data, config?)Parse IV, ciphertext, and tag from one binary input
AESSealedData.fromParts(iv, ciphertext, tag)Build from three binary inputs
AESSealedData.fromParts(iv, ciphertextWithTag, tagLength?)Split an appended tag, default 16 bytes
sealed.ivSize, sealed.tagSize, sealed.combinedSizeByte lengths
sealed.iv(encoding?), sealed.tag(encoding?)A promise of bytes, or a string with 'base64'
sealed.ciphertext({ encoding, includeTag })A promise of ciphertext as 'bytes' or 'base64', optionally with its tag
sealed.combined(encoding?)A promise of IV + ciphertext + tag as bytes, or a string with 'base64'
AESSealedDataConfigTypeDescription
ivLengthnumberIV prefix length in bytes; default 12.
numberIV prefix length in bytes; default 12.
tagLengthnumberAuthentication tag suffix length in bytes; default 16.
numberAuthentication tag suffix length in bytes; default 16.

The combined format carries no lengths. If encryption used a non-default IV or tag length, save those sizes alongside the payload and pass them to fromCombined. Additional authenticated data is also supplied separately. Decrypt rejects if the key, tag, ciphertext, or authenticated data does not match.

WebCrypto globals

GlobalsTypeDescription
crypto.getRandomValues(array: integer TypedArray) => same arrayFill the array with secure bytes, in place.
(array: integer TypedArray) => same arrayFill the array with secure bytes, in place.
crypto.randomUUID() => stringA v4 UUID from 16 secure bytes.
() => stringA v4 UUID from 16 secure bytes.
crypto.subtle.digest(algorithm: SHA name, data: BufferSource) => Promise<ArrayBuffer>Hash with SHA-1, 256, 384, or 512.
(algorithm: SHA name, data: BufferSource) => Promise<ArrayBuffer>Hash with SHA-1, 256, 384, or 512.

Platform differences

iOSAndroidWeb
RandomCSPRNGCSPRNGBrowser
DigestCommonCryptoJava digestBrowser
AES-GCMCommonCryptojavax.cryptoBrowser WebCrypto
MD5SupportedSupportedRejects
InstallStartupStartupIf missing

Limits and gotchas

Examples

Mint request ids

function newRequestId() {
return crypto.randomUUID()
}

One synchronous native call per id keeps hot paths allocation-free on the JavaScript side. The ids are v4, random on every call.

Hash a payload before upload

async function sha256Hex(data: Uint8Array<ArrayBuffer>) {
const hash = await crypto.subtle.digest('SHA-256', data)
return [...new Uint8Array(hash)]
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('')
}

The digest snapshots the input before hashing, so reusing or mutating the buffer after the call cannot corrupt the hash. The hex mapping runs on the resolved buffer.

Edit this page on GitHub.