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.
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.
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.
One.Crypto is available from import { One } from 'one'. The AES key and
sealed-data classes are named imports from one.
| Method | Returns |
|---|---|
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.
| DigestStringOptions | Type | Description |
|---|---|---|
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.
| AESEncryptOptions | Type | Description |
|---|---|---|
additionalDataBinaryInputAuthenticated data that is verified but not encrypted or stored in the sealed result. | BinaryInput | Authenticated 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. | GCMNonceParam | Omit 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. | number | Tag bytes: 16 by default, or 15, 14, 13, 12, 8, 4. Apple encryption always produces 16. |
| AESDecryptOptions | Type | Description |
|---|---|---|
additionalDataBinaryInputThe same authenticated data passed when encrypting. | BinaryInput | The 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. |
| Method | Returns |
|---|---|
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.
| Method or property | Result |
|---|---|
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.combinedSize | Byte 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' |
| AESSealedDataConfig | Type | Description |
|---|---|---|
ivLengthnumberIV prefix length in bytes; default 12. | number | IV prefix length in bytes; default 12. |
tagLengthnumberAuthentication tag suffix length in bytes; default 16. | number | Authentication 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.
| Globals | Type | Description |
|---|---|---|
crypto.getRandomValues(array: integer TypedArray) => same arrayFill the array with secure bytes, in place. | (array: integer TypedArray) => same array | Fill the array with secure bytes, in place. |
crypto.randomUUID() => stringA v4 UUID from 16 secure bytes. | () => string | A 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. |
| iOS | Android | Web | |
|---|---|---|---|
| Random | CSPRNG | CSPRNG | Browser |
| Digest | CommonCrypto | Java digest | Browser |
| AES-GCM | CommonCrypto | javax.crypto | Browser WebCrypto |
| MD5 | Supported | Supported | Rejects |
| Install | Startup | Startup | If missing |
subtle.digest. SHA-1, SHA-256, SHA-384, and SHA-512;
anything else rejects with NotSupportedError. One.Crypto adds MD5 on
native; treat MD5 as a checksum, never as security.nonce
generates one. Supplying explicit bytes makes uniqueness the caller’s responsibility.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.
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.