From 8b5abe02e2b9d3f3101afa014c4fa4655ed4b099 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 17 Jan 2025 21:06:55 +0100 Subject: [PATCH] feat(offline): add decrypt as zip util --- src/utils/zip.ts | 140 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 136 insertions(+), 4 deletions(-) diff --git a/src/utils/zip.ts b/src/utils/zip.ts index 577dd86..485c3ea 100644 --- a/src/utils/zip.ts +++ b/src/utils/zip.ts @@ -1,6 +1,12 @@ import JSZip from 'jszip' import { toast } from 'react-toastify' -import { InputFileFormat, OutputByType, OutputType } from '../types' +import { InputFileFormat, Meta, OutputByType, OutputType } from '../types' +import { NavigateOptions, To } from 'react-router-dom' +import { appPublicRoutes } from '../routes' +import { NostrController } from '../controllers' +import { decryptArrayBuffer } from './crypto' +import { hexToNpub, parseJson, SigitStatus, timeout } from '.' +import { SignerService } from '../services' /** * Read the content of a file within a zip archive. @@ -9,7 +15,7 @@ import { InputFileFormat, OutputByType, OutputType } from '../types' * @param outputType The type of output to return (e.g., 'string', 'arraybuffer', 'uint8array', etc.). * @returns A Promise resolving to the content of the file, or null if an error occurs. */ -const readContentOfZipEntry = async ( +export const readContentOfZipEntry = async ( zip: JSZip, filePath: string, outputType: T @@ -34,7 +40,7 @@ const readContentOfZipEntry = async ( }) } -const loadZip = async (data: InputFileFormat): Promise => { +export const loadZip = async (data: InputFileFormat): Promise => { try { return await JSZip.loadAsync(data) } catch (err) { @@ -46,4 +52,130 @@ const loadZip = async (data: InputFileFormat): Promise => { } } -export { readContentOfZipEntry, loadZip } +export const decrypt = async (file: File) => { + const nostrController = NostrController.getInstance() + + const zip = await loadZip(file) + if (!zip) return + + const keysFileContent = await readContentOfZipEntry( + zip, + 'keys.json', + 'string' + ) + + if (!keysFileContent) return null + + const parsedKeysJson = await parseJson<{ sender: string; keys: string[] }>( + keysFileContent + ).catch((err) => { + console.log(`Error parsing content of keys.json:`, err) + toast.error(err.message || `Error parsing content of keys.json`) + return null + }) + + if (!parsedKeysJson) return + + const encryptedArrayBuffer = await readContentOfZipEntry( + zip, + 'compressed.sigit', + 'arraybuffer' + ) + + if (!encryptedArrayBuffer) return + + const { keys, sender } = parsedKeysJson + + for (const key of keys) { + // decrypt the encryptionKey, with timeout (duration = 60 seconds) + const encryptionKey = await Promise.race([ + nostrController.nip04Decrypt(sender, key), + timeout(60000) + ]) + .then((res) => { + return res + }) + .catch((err) => { + console.log('err :>> ', err) + return null + }) + + // Return if encryption failed + if (!encryptionKey) continue + + const arrayBuffer = await decryptArrayBuffer( + encryptedArrayBuffer, + encryptionKey + ).catch((err) => { + console.log('err in decryption:>> ', err) + return null + }) + + if (arrayBuffer) return arrayBuffer + } + + return null +} + +type NavigateArgs = { to: To; options?: NavigateOptions } +export const navigateFromZip = async (file: File, pubkey: `npub1${string}`) => { + if (!file.name.endsWith('.sigit.zip')) { + toast.error(`Not a SiGit zip file: ${file.name}`) + } + + try { + let zip = await JSZip.loadAsync(file) + if (!zip) { + return null + } + + let arrayBuffer: ArrayBuffer | undefined + if ('keys.json' in zip.files) { + // Decrypt + const decryptedArrayBuffer = await decrypt(file).catch((err) => { + console.error(`error occurred in decryption`, err) + toast.error(err.message || `error occurred in decryption`) + }) + + if (decryptedArrayBuffer) { + // Replace the zip and continue processing + zip = await JSZip.loadAsync(decryptedArrayBuffer) + arrayBuffer = decryptedArrayBuffer + } + } + + if ('meta.json' in zip.files) { + // Check where we need to navigate + // Find Meta and process it for signer state + const metaContent = await readContentOfZipEntry( + zip, + 'meta.json', + 'string' + ) + if (metaContent) { + const meta = JSON.parse(metaContent) as Meta + const signerService = new SignerService(meta) + + const to = + signerService.getSignedStatus() === SigitStatus.Complete + ? appPublicRoutes.verify + : signerService.getNavigate(hexToNpub(pubkey)) + + return { + to, + options: { + state: { uploadedZip: arrayBuffer || file } + } + } as NavigateArgs + } + } + + return null + } catch (err) { + console.error('err in processing sigit zip file :>> ', err) + if (err instanceof Error) { + toast.error(err.message || 'An error occurred in loading zip file.') + } + return null + } +}