A demonstration app showcasing binary data manipulation across various Expo libraries using image data as an example. The app demonstrates encrypted image workflows, from client-side encryption to secure storage on external hosting and later retrieval.
demo.mp4
The app implements the following workflow:
- Upload Flow: User picks an image, encrypts it with a password before uploading to an external server
- Download Flow: User downloads the encrypted image, enters password, and the decrypted image is displayed
The demo extensively uses blobs and array buffers, integrating many Expo libraries including FileSystem, Image, Blob, Clipboard, ImagePicker, MediaLibrary, and more. It also demonstrates basic cryptography techniques like key derivation functions and AES-256-GCM encryption.
Note: UI styling is vibe-coded. Looking pretty is not the main purpose of this app.
-
Clone the repository
-
Install dependencies:
bun install
-
Build the app and start the development server:
bun ios # or bun android
- iOS:
bun ios - Android:
bun android - Web:
bun web(automatically starts CORS proxy)
The app provides two main screens accessible via tabs:
- Upload: Pick an image from gallery, encrypt it with a password, and upload to a server
- Download: Download and decrypt previously uploaded encrypted images
- AES Playground: Direct testing of AES encryption/decryption functionality
Most of the relevant code is in the
src/business-logic/
to keep all the interesting code in one place.
├── src/
│ ├── app/ # Expo Router app structure
│ │ ├── (tabs)/ # Tab-based navigation
│ │ │ ├── upload.tsx # Upload workflow screen
│ │ │ └── download.tsx # Download workflow screen
│ ├── business-logic/ # Core app logic
│ ├── components/ # Reusable UI components
│ ├── hooks/ # Custom React hooks
│ ├── utils/ # Utility functions
│ └── imports/ # Platform-specific imports with web workarounds
├── modules/
│ ├── expo-argon2/ # Expo Module for Argon2 hashing (git submodule)
│ └── image-loader/ # Custom image loading utilities
├── cors-proxy/ # CORS proxy server for web downloads
└── assets/ # Static assets (images, etc.)
Now removed. Since SDK 55, expo-crypto includes AES encryption.
- expo-crypto docs
- original pull request
Custom module for converting Uint8Array to SharedRef<'image'>, enabling
direct image manipulation from binary data. Neither expo-image
or expo-image-manipulator is able to do that directly, without filesystem-
or base64-data-url-intermediates, or other workarounds.
Git submodule for expo-argon2 - implementation of the Argon2 Key derivation function, the most recommended password hashing algorithm.
Based on development experience and code analysis, several issues and limitations have been identified:
-
iOS/Android:
- FileSystem file creation for user-picked directory (outside documents/cache dir) differ significantly. Probably due to SAF. It might be good to create some documentation / examples.
-
iOS:
expo-imageenforces ATS (App Transport Security) -Image.loadAsync()withhttp://URLs doesn't work. Onlyhttps://URLs accepted. I have not found any documentation about this. Bad thing is that the image just silently fails to render, there's no developer warning.
-
Android:
FileSystem.Directory.createFile(), whenmimeTypeargument is null, ignores file extension, forcestext/plain→.txtextension regardless of filename likeimage.jpg. SAF (Storage Access Framework) limitation, but perhaps we could best-effort determine MIME type?. (code link)- MediaLibrary asset creation permissions:
MediaLibrary.Asset.create()doesn't work withwriteOnly: true, granularPermissions: ['photo']. It requireswriteOnly: falsewhich is counter-intuitive since I want only to create asset, not read it. Perhaps should be better documented.
-
Web:
Fixed in #41195. Until the fix is released,expo-blobimport fails:TypeError: _expoBlob.Blob is not a constructor.bun patchis used in this repo.ExpoClipboardlistener on web. Error:TypeError: ExpoClipboard.default.addListener is not a function. Not sure if this is my mistake or some other issue.- Interesting fact is that the function is named
addClipboardListenerbut it's somewhere translated toaddListenerwhich doesn't exist.
- Interesting fact is that the function is named
expo-media-library/nexthas no web implementation, causing bundler errors when imported.
- Base64 UTF-16 conversion bottleneck:
atob()returns UTF-16 string,String.split('').map(c => c.charCodeAt(0))creates intermediate array + function calls for each character. For 1MB base64 (~1.33MB string), this creates 1.33M temporary objects. Optimizedfor(let i=0; i<len; i++) bytes[i] = str.charCodeAt(i)reduces to ~35-40ms but still 2-3x slower than hypothetical nativeatob()→Uint8Arrayconversion.- Possible solution: try implementing Uint8Array base64 encode-decode in native code.
- Streams API regression (expo-blob, expo-file-system):
ReadableStream.pipeTo(WritableStream)processes in chunks with async coordination overhead. Each chunk requires:await reader.read()→await writer.write()→ yield to event loop. For 3MB blob = ~7s processing time. Directblob.arrayBuffer()+file.write(buffer)bypasses chunking: single allocation + native write = ~11ms.
Binary Data Conversion Issues:
- Expo-fetch
FormDatadoesn't supportexpo-blobfilenames. A workaround with.nameis required. This was noted in #40586 too. - No native
Uint8Array→SharedRef<'image'>conversion: Expo's architecture lacks direct binary-to-ImageRef conversion. Theexpo-imagemodule only accepts URIs or assets, not raw binary data. Custom ImageLoader module was necessary to fs/data-url workarounds. - Limited base64 ecosystem support:
- Legacy
FileSystem.writeAsStringAsync()requiresencoding: FileSystem.EncodingType.Base64but many APIs don't accept this parameter - No easy obvious way of converting base64 string into blob or array buffer, and vice versa. And, as mentioned above,
atob()/btoa()lack performance.
- Legacy
expo-clipboardcould acceptImageReffor copying and allow pasting asImageReftoo. It would be more convenient than base64.
File System Limitations:
- No save-as dialog equivalent:
FileSystem.File.pickFile()is read-only. No write equivalent toinput[type="file"]save behavior. Workaround:FileSystem.Directory.pickAsync()+ hardcoded filename, but user cannot specify filename.
Platform Integration Issues:
SharedRefisolation between modules: Expo modules use opaqueSharedRef<T>pointers. Each module has its own nativeSharedRef<'image'>implementation, that differs slightly in terms of member methods/properties.- For instance:
- expo-image's ImageRef has
loadAsync()method, which is missing in expo-image-manipulator. - expo-image-manipulator implementation has
saveAsync()which is missing in expo-image.
- expo-image's ImageRef has
- It would be nice to have a way to somehow move between implementations from JS code.
- For instance:
- Inconsistent MIME type detection:
FileSystem.File.typeproperty returnsnullfor newly created files (also afterfile.write()) until platform file system updates metadata. Is there a way to force-flush written content and trigger MIME detection? Something likeFile.reload()could be helpful.ExpoImage.loadAsync().mediaTypeis unavailable on Android (expected).- When working with array buffers, often manual magic byte detection is required to infer MIME type or file extension. (this function)
- In this demo, this is more an app-specific issue: it's caused by the fact that original image metadata is lost when saving encrypted data to file, instead of uploading it to tmpfiles (there metadata is held in the context value).
The following TODO/FIXME comments address issues not necessarily covered in the limitations above:
Potential subjects to deprecation:
- Clipboard event's
contentproperty has been deprecated for very long. Should it be finally removed? - Clipboard has the
Clipboard.removeClipboardListener(listener)function. AFAIK, APIs generally migrate from this syntax to thesubsciption.remove(). Should this one be deprecated too?
The demo uses a file hosting service missing CORS headers, causing web downloads to fail. A CORS proxy server resolves this issue.
When running the web version (bun web), the CORS proxy is automatically started alongside the Expo bundler using concurrently. This ensures seamless web development without manual proxy setup.
See cors-proxy/README.md for detailed setup and usage instructions.
This is a demonstration project. Check individual dependencies for their respective licenses.