Handover
Architecture
An Expo app that interacts with encrypted files (image, video, audio, text) locally and in the cloud.
Encryption
Uses CHACHA20-POLY1305 for encryption and decryption with libsodium (a C library). Most of this is wrapped in useFileEncryption() and pulls keys from useEncryptedKeys()
Currently, the files are loaded into memory, converted to base64, encrypted and stored as blob_${id} in the document's sdirectory. (This can be improved further down the line with stream encryption)
CHACHA20-POLY1305 uses a public key, a public nonce and a secret key to encrypt and decrypt. However in the libsodium library there's also an "additionalData" that seems to break if it's not passed in (for now it's "armourvault")
const encryptProps = {
message: sodium.from_base64(sodium.to_base64(props.message)),
key: sodium.from_base64(props.keyInBase64),
additionalData: "armourvault",
secretNonce: props.secretNonceInBase64
? sodium.from_base64(props.secretNonceInBase64)
: null,
publicNonce: sodium.from_base64(props.publicNonceInBase64),
};File traversal
Files metadata are stored in an SQLite database, this acts as a source of truth for how files are stored.
export const metadata = sqliteTable("file_metadata", {
ulid: text("ulid").primaryKey(),
publicNonceInBase64: text("public_nonce_in_base64").notNull(),
blobPath: text("blob_path").notNull(),
fakePath: text("fake_path").notNull(),
sizeInBytes: integer("size_in_bytes").notNull(),
});Any file manipulation uses this database to track what's been done and any mutations to a file is recorded in this table.
It was designed in a way that the blobPath (the encrypted file) can be stored anywhere (locally or on the cloud) and the fakePath acts UNIX pattern to store directory and files
This needs to be extended to support duress functionality (another field that references a new durress table would work)
Folder traversal
Currently, to traverse the file, the starting point is / and any direct children of that folder is rendered. Creating a new file is just adding a new entry with the fakePath ending in / (e.g. /images/2014/). Deleting a folder (to be done) would require all the children of that path to be deleted (will need to get all affected rows, loop through them and delete the blobPath contents)
File types with viewing, creating and importing
To support a new type of file, the folder structure normally looks like
▾ [ ]images/
[ ]create.tsx
[ ]file-item.tsx
[ ]icon.tsx
[ ]import.tsx
[ ]index.tsx
[ ]view.tsxcreate.tsx: Usually a button to create a new file.
file-item.tsx: How this file type is rendered in the directory list
icon.tsx: The icon for the file type
import.tsx: A button to import a file if it's not created (a common pattern if the file is already created from somewhere else)
view.tsx: The view of the file
index.ts: The barrel output of the files (optional)
The above structure is flexible depending on how the file is meant to be supported (some have no import functionality). However because most files share this structure, the consumption of these different components can be seen in the below files
// app/home/files/index.tsx
export default function FilePage() {
return (
<Layout.Home title="All Files">
<FileContent />
<Videos.DialogVideoViewer />
<Images.DialogImageViewer />
<Images.DialogLoadingImportingImages />
<Audio.BottomSheetAudioPlayer />
<Audio.BottomSheetAudioRecorder />
<Unknown.DialogUnknownViewDownload />
</Layout.Home>
);
}// modules/files/meta/index.tsx
export function FileItem(file: z.infer<typeof metadataZodSchema.select>) {
const mimeType = getMimeType(file.fakePath);
if (mimeType === "image") {
return <Images.FileImageItem file={file} />;
}
if (mimeType === "video") {
return <Videos.FileVideoItem file={file} />;
}
if (mimeType === "audio") {
return <Audio.FileAudioItem file={file} />;
}
if (mimeType === "document/pdf") {
return <Documents.FileDocumentPDFItem file={file} />;
}
if (mimeType === "document/md") {
return <Documents.FileDocumentMarkdownItem file={file} />;
}
if (mimeType === "folder") {
return <Folder.FolderItem file={file} />;
}
return <UnknownFileItem file={file} />;
}// modules/files/meta/bottom-sheet/add-file.tsx
export function AddFile() {
return (
<View className="gap-[21px]">
<Text className="text-sub font-400">Import</Text>
<View
style={{ rowGap: 12, columnGap: 20 }}
className="mt-[10px] flex-row justify-between flex-wrap max-w-[400px]"
>
<ImportImage />
<ImportDocument />
<View className="w-[58px]" />
<View className="w-[58px]" />
</View>
<Text className="text-sub font-400">Create</Text>
<View
style={{ rowGap: 12, columnGap: 20 }}
className="mt-[10px] flex-row justify-between flex-wrap max-w-[400px]"
>
<CreatePhoto />
<CreateAudio />
<CreateText />
<CreateFolder />
</View>
</View>
);
}Source code structure
I try to follow Bulletproof React as close as I can, but instead of features it's called modules, app is expo router specific and its how the app knows how to navigate. Most of the action is under modules/files since the app is designed to support many different types
ui is react-native-reusables components, so they're all easily extendable libs are any global libraries used across more than one module.
At a high level, most application code is baked into modules/<...> and app/<...> should just consume it and not know the implementation details. This way if someone new gets onboarded they don't have to shift through a lot to understand what's going on
Also I try to coalocate the functionality together in one file before splitting the components to different files.
Authentication
This is mostly done, just need to add some logic to consume the authenticated state, so far the work done is just to allow a user to be authenticated.
Styling
We're using nativewind v4, it's good for the commonly used styles, but if you need to use some animations, then opt in to reanimated and pass it into styles
CICD
We use Expo and EAS to handle the CICD to Testflight (and maybe Google Play store if we're ready). The commmands to build should be in eas.json and access to it should just work
We also use Github and branching, for now it's just main but once we start releasing to production it'll be good to trigger Expo builds on certain events (like tagging triggers production builds etc...)
Linting / Testing
I haven't priortized this yet but it you do opt in for this down line, I liked biome.js and vitest