Skip to content

Commit

Permalink
feat(functions): add onFileUploadEncrypt server func
Browse files Browse the repository at this point in the history
  • Loading branch information
Aaron Teo committed Feb 11, 2024
1 parent 25d808e commit 6659939
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 5 deletions.
15 changes: 10 additions & 5 deletions apps/backend/src/lib/components/FileUpload.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
import { authStore } from '$lib/stores';
import { colFilesRef } from '$lib/firebase/firestore';
import { ref, getStorage, uploadBytes } from 'firebase/storage';
import { fileStorage } from '$lib/firebase/storage';
import { ref, uploadBytes } from 'firebase/storage';
import { doc, setDoc, serverTimestamp } from 'firebase/firestore';
interface FormValues {
Expand Down Expand Up @@ -48,10 +49,12 @@
}),
onSubmit: async (data) => {
// Just to please TypeScript because we are already using validationSchema above.
if (!$authStore) return;
if (!data.fileUpload || !data.fileClass || !data.isChecked || !data.filePassword) return;
const fileUniqueId = v4();
const formFile = data.fileUpload[0];
const formFileName = v4();
const formFileExt = formFile.name.split('.').pop() || '';
const formFileBuffer = await formFile.arrayBuffer();
const filePwdBuffer = new TextEncoder().encode(data.filePassword);
Expand All @@ -62,21 +65,23 @@
.join('');
const fileData: FSFile = {
file_id: fileUniqueId,
file_id: formFileName,
file_status: 'UPLOADED',
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
file_domain: $authStore?.email?.split('@').pop()!, // TODO: Please change later
file_classification: data.fileClass!,
file_name: formFile.name,
file_ext: formFile.name.split('.').pop() || '',
file_owner_id: $authStore!.uid,
file_encryption_hash: filePwdHash,
file_encryption_iv: '',
file_permissions: [],
updated_at: serverTimestamp(),
created_at: serverTimestamp(),
};
const docRef = doc(colFilesRef);
const storageRef = ref(getStorage(), `armadillo-files/${fileUniqueId}`);
const docRef = doc(colFilesRef, formFileName);
const storageRef = ref(fileStorage, `${$authStore.uid}/${formFileName}.${formFileExt}`);
const setDocPromise = setDoc(docRef, fileData);
const uploadDocPromise = uploadBytes(storageRef, formFileBuffer);
Expand Down
1 change: 1 addition & 0 deletions packages/functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export * from './https/onCall/rekognition/getAuthToken';
export * from './https/onCall/file/getPassword';

export * from './storage/onObjectFinalized/user/onHeadshotUpload';
export * from './storage/onObjectFinalized/file/onFileUploadEncrypt';
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { FSFile } from '@armadillo/shared';

import { BUCKET_FILES, FS_COLLECTION_FILES } from '@armadillo/shared';

import { subtle, getRandomValues } from 'crypto';
import { parse, basename, dirname } from 'path';

import { FieldValue } from 'firebase-admin/firestore';
import { logger } from 'firebase-functions/v2';
import { onObjectFinalized } from 'firebase-functions/v2/storage';

import { bucketFiles, firestore } from '../../../firebase';

export const storage_onObjectFinalized_file_onFileUploadEncrypt = onObjectFinalized(
{
bucket: BUCKET_FILES,
},
async ({ data }) => {
const { name, size, contentType } = data;

// Checks if the file uploaded is a directory
if (!contentType || contentType === 'application/octet-stream')
return logger.log(
`File ${name} is being ignored. It is either a folder creation, or does not have a valid content type!`
);

// Checks if the file uploaded is larger than 32MB
if (size > 32 * 33554432)
return logger.log(`File ${name} is being ignored. It is bigger than 32MB!`);

const fileName = basename(name);
const fileOwner = basename(dirname(name));
if (fileName.startsWith('enc_')) return logger.log(`File ${fileName} is already encrypted!`);
if (fileOwner === '.') return logger.log(`File ${fileName} is not owned by any user!`);

const { name: fsFileName } = parse(fileName);
const fsFileRef = firestore.collection(FS_COLLECTION_FILES).doc(fsFileName);
const fsFileSnapshot = await fsFileRef.get();
const fsFileData = fsFileSnapshot.data() as FSFile;

if (!fsFileSnapshot.exists)
return logger.log(`File ${fsFileSnapshot.ref.path} does not exist in Firestore!`);

const csFileRef = bucketFiles.file(name);
const csFileExists = await csFileRef.exists();
if (!csFileExists)
return logger.log(
`File ${JSON.stringify(csFileRef.name, null, 2)} does not exist in Cloud Storage!`
);

const [csFileObject] = await bucketFiles.file(name).download();
const fsFileKey = fsFileData.file_encryption_hash;
const fsFileKeyArray = Uint8Array.from(Buffer.from(fsFileKey, 'hex'));
const fsFileKeyCrypto = await subtle.importKey(
'raw',
fsFileKeyArray,
{ name: 'AES-CBC' },
false,
['encrypt']
);

const fsFileIv = getRandomValues(new Uint8Array(16));
const fsFileIvHex = Buffer.from(fsFileIv).toString('hex');
const fsFileEncObject = await subtle.encrypt(
{ name: 'AES-CBC', iv: fsFileIv },
fsFileKeyCrypto,
csFileObject
);

const csFileEncName = `${fileOwner}/enc_${fileName}`;
const csFileEncRef = bucketFiles.file(csFileEncName);

try {
await Promise.all([csFileRef.delete(), csFileEncRef.save(Buffer.from(fsFileEncObject))]);
} catch (error) {
logger.error(`Error encrypting file ${fileName}!`);
logger.error(error);
}

const fsFileUpdate: Partial<FSFile> = {
file_status: 'ENCRYPTED',
file_encryption_iv: fsFileIvHex,
updated_at: FieldValue.serverTimestamp(),
};

return fsFileRef.set(fsFileUpdate, { merge: true });
}
);
2 changes: 2 additions & 0 deletions packages/shared/models/FSFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import type { FSMetadata, FSFileClass } from '@armadillo/shared';

interface FSFile extends FSMetadata {
readonly file_id: string;
readonly file_status: 'UPLOADED' | 'ENCRYPTED' | 'READY';
readonly file_domain: string;
readonly file_classification: FSFileClass;
readonly file_name: string;
readonly file_ext: string;
readonly file_owner_id: string;
readonly file_encryption_hash: string;
readonly file_encryption_iv: string;
readonly file_permissions: string[];
}

Expand Down

0 comments on commit 6659939

Please sign in to comment.