fix: sigit's wrapper zip should contain keys.json file

This commit is contained in:
SwiftHawk 2024-06-12 15:02:26 +05:00
parent b145624f4c
commit ded8304c66
4 changed files with 364 additions and 130 deletions

View File

@ -378,6 +378,54 @@ export class NostrController extends EventEmitter {
throw new Error('Login method is undefined')
}
nip04Decrypt = async (sender: string, content: string) => {
const loginMethod = (store.getState().auth as AuthState).loginMethod
if (loginMethod === LoginMethods.extension) {
const nostr = this.getNostrObject()
if (!nostr.nip04) {
throw new Error(
`Your nostr extension does not support nip04 encryption & decryption`
)
}
const decrypted = await nostr.nip04.decrypt(sender, content)
return decrypted
}
if (loginMethod === LoginMethods.privateKey) {
const keys = (store.getState().auth as AuthState).keyPair
if (!keys) {
throw new Error(
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
)
}
const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array
const decrypted = await nip04.decrypt(privateKey, sender, content)
return decrypted
}
if (loginMethod === LoginMethods.nsecBunker) {
const user = new NDKUser({ pubkey: sender })
this.remoteSigner?.on('authUrl', (authUrl) => {
this.emit('nsecbunker-auth', authUrl)
})
if (!this.remoteSigner) throw new Error('Remote signer is undefined.')
const decrypted = await this.remoteSigner.decrypt(user, content)
return decrypted
}
throw new Error('Login method is undefined')
}
/**
* Function will capture the public key from the nostr extension or if no extension present
* function wil capture the public key from the local storage

View File

@ -33,6 +33,7 @@ import { Meta, ProfileMetadata, User, UserRole } from '../../types'
import {
encryptArrayBuffer,
generateEncryptionKey,
generateKeysFile,
getHash,
hexToNpub,
isOnline,
@ -49,15 +50,12 @@ import { HTML5Backend } from 'react-dnd-html5-backend'
import type { Identifier, XYCoord } from 'dnd-core'
import { useDrag, useDrop } from 'react-dnd'
import saveAs from 'file-saver'
import CopyModal from '../../components/copyModal'
import { Event, kinds } from 'nostr-tools'
export const CreatePage = () => {
const navigate = useNavigate()
const [isLoading, setIsLoading] = useState(false)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const [openCopyModal, setOpenCopyModel] = useState(false)
const [textToCopy, setTextToCopy] = useState('')
const [authUrl, setAuthUrl] = useState<string>()
@ -374,26 +372,68 @@ export const CreatePage = () => {
encryptionKey: string
): Promise<ArrayBuffer> => {
setLoadingSpinnerDesc('Encrypting zip file')
return encryptArrayBuffer(arraybuffer, encryptionKey).finally(() =>
setIsLoading(false)
return encryptArrayBuffer(arraybuffer, encryptionKey)
}
// create final zip file
const createFinalZipFile = async (
encryptedArrayBuffer: ArrayBuffer,
encryptionKey: string
): Promise<File | null> => {
// Get the current timestamp in seconds
const unixNow = Math.floor(Date.now() / 1000)
const blob = new Blob([encryptedArrayBuffer])
// Create a File object with the Blob data
const file = new File([blob], `compressed.sigit`, {
type: 'application/sigit'
})
const firstSigner = users.filter((user) => user.role === UserRole.signer)[0]
const keysFileContent = await generateKeysFile(
[firstSigner.pubkey],
encryptionKey
)
if (!keysFileContent) return null
const zip = new JSZip()
zip.file(`compressed.sigit`, file)
zip.file('keys.json', keysFileContent)
const arraybuffer = await zip
.generateAsync({
type: 'arraybuffer',
compression: 'DEFLATE',
compressionOptions: { level: 6 }
})
.catch(handleZipError)
if (!arraybuffer) return null
const finalZipFile = new File(
[new Blob([arraybuffer])],
`${unixNow}.sigit.zip`,
{
type: 'application/zip'
}
)
return finalZipFile
}
// Handle file upload and further actions based on online/offline status
const handleFileUpload = async (blob: Blob, encryptionKey: string) => {
const handleFileUpload = async (file: File, arrayBuffer: ArrayBuffer) => {
if (await isOnline()) {
const fileUrl = await uploadFile(blob)
const fileUrl = await uploadFile(file)
if (!fileUrl) return
await sendDMs(fileUrl, encryptionKey)
await sendDMs(fileUrl)
setIsLoading(false)
navigate(
`${appPrivateRoutes.sign}?file=${encodeURIComponent(fileUrl)}&key=${encodeURIComponent(encryptionKey)}`
)
navigate(appPrivateRoutes.sign, { state: { arrayBuffer } })
} else {
handleOffline(blob, encryptionKey)
handleOffline(file, arrayBuffer)
}
}
@ -406,11 +446,11 @@ export const CreatePage = () => {
}
// Upload the file to the storage and send DMs to signers/viewers
const uploadFile = async (blob: Blob): Promise<string | null> => {
const uploadFile = async (file: File): Promise<string | null> => {
setIsLoading(true)
setLoadingSpinnerDesc('Uploading zip file to file storage.')
const fileUrl = await uploadToFileStorage(blob, nostrController)
const fileUrl = await uploadToFileStorage(file, nostrController)
.then((url) => {
toast.success('zip file uploaded to file storage')
return url
@ -420,8 +460,8 @@ export const CreatePage = () => {
return fileUrl
}
// Send DMs to signers and viewers with the file URL and encryption key
const sendDMs = async (fileUrl: string, encryptionKey: string) => {
// Send DMs to signers and viewers with the file URL
const sendDMs = async (fileUrl: string) => {
setLoadingSpinnerDesc('Sending DM to signers/viewers')
const signers = users.filter((user) => user.role === UserRole.signer)
@ -430,7 +470,6 @@ export const CreatePage = () => {
if (signers.length > 0) {
await sendDM(
fileUrl,
encryptionKey,
signers[0].pubkey,
nostrController,
true,
@ -438,34 +477,15 @@ export const CreatePage = () => {
)
} else {
for (const viewer of viewers) {
await sendDM(
fileUrl,
encryptionKey,
viewer.pubkey,
nostrController,
false,
setAuthUrl
)
await sendDM(fileUrl, viewer.pubkey, nostrController, false, setAuthUrl)
}
}
}
// Manage offline scenarios for signing or viewing the file
const handleOffline = (blob: Blob, encryptionKey: string) => {
const signers = users.filter((user) => user.role === UserRole.signer)
if (signers[0] && signers[0].pubkey === usersPubkey) {
// Create a File object with the Blob data for offline signing
const file = new File([blob], `compressed.sigit`, {
type: 'application/sigit'
})
navigate(appPrivateRoutes.sign, { state: { file, encryptionKey } })
} else {
// Save the file and show encryption key for offline viewing
saveAs(blob, 'request.sigit')
setTextToCopy(encryptionKey)
setOpenCopyModel(true)
}
const handleOffline = (file: File, arrayBuffer: ArrayBuffer) => {
saveAs(file, 'request.sigit.zip')
navigate(appPrivateRoutes.sign, { state: { arrayBuffer } })
}
const handleCreate = async () => {
@ -497,9 +517,15 @@ export const CreatePage = () => {
arraybuffer,
encryptionKey
)
const blob = new Blob([encryptedArrayBuffer])
return await handleFileUpload(blob, encryptionKey)
const finalZipFile = await createFinalZipFile(
encryptedArrayBuffer,
encryptionKey
)
if (!finalZipFile) return
return await handleFileUpload(finalZipFile, arraybuffer)
}
if (authUrl) {
@ -596,15 +622,6 @@ export const CreatePage = () => {
</Button>
</Box>
</Box>
<CopyModal
open={openCopyModal}
handleClose={() => {
setOpenCopyModel(false)
navigate(appPrivateRoutes.sign)
}}
title="Decryption key for Sigit file"
textToCopy={textToCopy}
/>
</>
)
}

View File

@ -10,7 +10,6 @@ import {
TableCell,
TableHead,
TableRow,
TextField,
Tooltip,
Typography,
useTheme
@ -52,7 +51,8 @@ import {
shorten,
signEventForMetaFile,
uploadToFileStorage,
isOnline
isOnline,
generateKeysFile
} from '../../utils'
import styles from './style.module.scss'
import {
@ -71,14 +71,13 @@ enum SignedStatus {
export const SignPage = () => {
const navigate = useNavigate()
const location = useLocation()
const { file, encryptionKey: encKey } = location.state || {}
const { arrayBuffer: decryptedArrayBuffer } = location.state || {}
const [searchParams, setSearchParams] = useSearchParams()
const [searchParams] = useSearchParams()
const [displayInput, setDisplayInput] = useState(false)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [encryptionKey, setEncryptionKey] = useState('')
const [zip, setZip] = useState<JSZip>()
@ -194,9 +193,8 @@ export const SignPage = () => {
useEffect(() => {
const fileUrl = searchParams.get('file')
const key = searchParams.get('key')
if (fileUrl && key) {
if (fileUrl) {
setIsLoading(true)
setLoadingSpinnerDesc('Fetching file from file server')
@ -208,7 +206,7 @@ export const SignPage = () => {
const fileName = fileUrl.split('/').pop()
const file = new File([res.data], fileName!)
decrypt(file, decodeURIComponent(key)).then((arrayBuffer) => {
decrypt(file).then((arrayBuffer) => {
if (arrayBuffer) handleDecryptedArrayBuffer(arrayBuffer)
})
})
@ -221,40 +219,109 @@ export const SignPage = () => {
.finally(() => {
setIsLoading(false)
})
} else if (file && encKey) {
decrypt(file, decodeURIComponent(encKey))
.then((arrayBuffer) => {
if (arrayBuffer) handleDecryptedArrayBuffer(arrayBuffer)
})
.catch((err) => {
console.error(`error occurred in decryption`, err)
toast.error(err.message || `error occurred in decryption`)
})
.finally(() => {
setIsLoading(false)
})
} else if (decryptedArrayBuffer) {
handleDecryptedArrayBuffer(decryptedArrayBuffer).finally(() =>
setIsLoading(false)
)
} else {
setIsLoading(false)
setDisplayInput(true)
}
}, [searchParams, file, encKey])
}, [searchParams, decryptedArrayBuffer])
const decrypt = async (file: File, key: string) => {
const parseKeysJson = async (zip: JSZip) => {
const keysFileContent = await readContentOfZipEntry(
zip,
'keys.json',
'string'
)
if (!keysFileContent) return null
const parsedJSON = 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
})
return parsedJSON
}
const decrypt = async (file: File) => {
setLoadingSpinnerDesc('Decrypting file')
const encryptedArrayBuffer = await file.arrayBuffer()
const zip = await JSZip.loadAsync(file).catch((err) => {
console.log('err in loading zip file :>> ', err)
toast.error(err.message || 'An error occurred in loading zip file.')
return null
})
if (!zip) return
const arrayBuffer = await decryptArrayBuffer(encryptedArrayBuffer, key)
.catch((err) => {
console.log('err in decryption:>> ', err)
toast.error(err.message || 'An error occurred in decrypting file.')
return null
})
.finally(() => {
setIsLoading(false)
const parsedKeysJson = await parseKeysJson(zip)
if (!parsedKeysJson) return
const encryptedArrayBuffer = await readContentOfZipEntry(
zip,
'compressed.sigit',
'arraybuffer'
)
if (!encryptedArrayBuffer) return
const { keys, sender } = parsedKeysJson
for (const key of keys) {
// Set up event listener for authentication event
nostrController.on('nsecbunker-auth', (url) => {
setAuthUrl(url)
})
return arrayBuffer
// Set up timeout promise to handle encryption timeout
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error('Timeout occurred'))
}, 60000) // Timeout duration = 60 seconds
})
// decrypt the encryptionKey, with timeout
const encryptionKey = await Promise.race([
nostrController.nip04Decrypt(sender, key),
timeoutPromise
])
.then((res) => {
return res
})
.catch((err) => {
console.log('err :>> ', err)
return null
})
.finally(() => {
setAuthUrl(undefined) // Clear authentication URL
})
console.log('encryptionKey :>> ', encryptionKey)
// Return if encryption failed
if (!encryptionKey) continue
const arrayBuffer = await decryptArrayBuffer(
encryptedArrayBuffer,
encryptionKey
)
.catch((err) => {
console.log('err in decryption:>> ', err)
return null
})
.finally(() => {
setIsLoading(false)
})
if (arrayBuffer) return arrayBuffer
}
return null
}
const handleDecryptedArrayBuffer = async (arrayBuffer: ArrayBuffer) => {
@ -348,13 +415,10 @@ export const SignPage = () => {
}
const handleDecrypt = async () => {
if (!selectedFile || !encryptionKey) return
if (!selectedFile) return
setIsLoading(true)
const arrayBuffer = await decrypt(
selectedFile,
decodeURIComponent(encryptionKey)
)
const arrayBuffer = await decrypt(selectedFile)
if (!arrayBuffer) return
@ -407,13 +471,15 @@ export const SignPage = () => {
setLoadingSpinnerDesc('Encrypting zip file')
const encryptedArrayBuffer = await encryptArrayBuffer(arrayBuffer, key)
const blob = new Blob([encryptedArrayBuffer])
const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key)
if (!finalZipFile) return
if (await isOnline()) {
await handleOnlineFlow(blob, key)
} else {
handleDecryptedArrayBuffer(arrayBuffer).finally(() => setIsLoading(false))
await handleOnlineFlow(finalZipFile)
}
handleDecryptedArrayBuffer(arrayBuffer).finally(() => setIsLoading(false))
}
// Read the content of the hashes.json file
@ -491,32 +557,101 @@ export const SignPage = () => {
})
}
// create final zip file
const createFinalZipFile = async (
encryptedArrayBuffer: ArrayBuffer,
encryptionKey: string
): Promise<File | null> => {
// Get the current timestamp in seconds
const unixNow = Math.floor(Date.now() / 1000)
const blob = new Blob([encryptedArrayBuffer])
// Create a File object with the Blob data
const file = new File([blob], `compressed.sigit`, {
type: 'application/sigit'
})
const isLastSigner = checkIsLastSigner(signers)
const userSet = new Set<string>()
if (isLastSigner) {
if (submittedBy) {
userSet.add(submittedBy)
}
signers.forEach((signer) => {
userSet.add(npubToHex(signer)!)
})
viewers.forEach((viewer) => {
userSet.add(npubToHex(viewer)!)
})
} else {
const usersNpub = hexToNpub(usersPubkey!)
const signerIndex = signers.indexOf(usersNpub)
const nextSigner = signers[signerIndex + 1]
userSet.add(npubToHex(nextSigner)!)
}
const keysFileContent = await generateKeysFile(
Array.from(userSet),
encryptionKey
)
if (!keysFileContent) return null
const zip = new JSZip()
zip.file(`compressed.sigit`, file)
zip.file('keys.json', keysFileContent)
const arraybuffer = await zip
.generateAsync({
type: 'arraybuffer',
compression: 'DEFLATE',
compressionOptions: { level: 6 }
})
.catch(handleZipError)
if (!arraybuffer) return null
const finalZipFile = new File(
[new Blob([arraybuffer])],
`${unixNow}.sigit.zip`,
{
type: 'application/zip'
}
)
return finalZipFile
}
// Handle errors during zip file generation
const handleZipError = (err: any) => {
console.log('Error in zip:>> ', err)
setIsLoading(false)
toast.error(err.message || 'Error occurred in generating zip file')
return null
}
// Handle the online flow: upload file and send DMs
const handleOnlineFlow = async (blob: Blob, key: string) => {
const fileUrl = await uploadZipFile(blob)
const handleOnlineFlow = async (file: File) => {
const fileUrl = await uploadZipFile(file)
if (!fileUrl) return
const isLastSigner = checkIsLastSigner(signers)
if (isLastSigner) {
await sendDMToAllUsers(fileUrl, key)
await sendDMToAllUsers(fileUrl)
} else {
await sendDMToNextSigner(fileUrl, key)
await sendDMToNextSigner(fileUrl)
}
setIsLoading(false)
// Update search params with updated file URL and encryption key
setSearchParams({
file: fileUrl,
key: key
})
}
// Upload the zip file to file storage
const uploadZipFile = async (blob: Blob): Promise<string | null> => {
const uploadZipFile = async (file: File): Promise<string | null> => {
setLoadingSpinnerDesc('Uploading zip file to file storage.')
const fileUrl = await uploadToFileStorage(blob, nostrController)
const fileUrl = await uploadToFileStorage(file, nostrController)
.then((url) => {
toast.success('Zip file uploaded to file storage')
return url
@ -540,7 +675,7 @@ export const SignPage = () => {
}
// Send DM to all users (signers and viewers)
const sendDMToAllUsers = async (fileUrl: string, key: string) => {
const sendDMToAllUsers = async (fileUrl: string) => {
const userSet = new Set<`npub1${string}`>()
if (submittedBy) {
@ -560,7 +695,6 @@ export const SignPage = () => {
for (const user of users) {
await sendDM(
fileUrl,
key,
npubToHex(user)!,
nostrController,
false,
@ -570,13 +704,12 @@ export const SignPage = () => {
}
// Send DM to the next signer
const sendDMToNextSigner = async (fileUrl: string, key: string) => {
const sendDMToNextSigner = async (fileUrl: string) => {
const usersNpub = hexToNpub(usersPubkey!)
const signerIndex = signers.indexOf(usersNpub)
const nextSigner = signers[signerIndex + 1]
await sendDM(
fileUrl,
key,
npubToHex(nextSigner)!,
nostrController,
true,
@ -771,18 +904,9 @@ export const SignPage = () => {
value={selectedFile}
onChange={(value) => setSelectedFile(value)}
/>
{selectedFile && (
<TextField
label="Encryption Key"
variant="outlined"
value={encryptionKey}
onChange={(e) => setEncryptionKey(e.target.value)}
/>
)}
</Box>
{selectedFile && encryptionKey && (
{selectedFile && (
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleDecrypt} variant="contained">
Decrypt

View File

@ -1,5 +1,10 @@
import axios from 'axios'
import { EventTemplate } from 'nostr-tools'
import {
EventTemplate,
generateSecretKey,
getPublicKey,
nip04
} from 'nostr-tools'
import { MetadataController, NostrController } from '../controllers'
import { toast } from 'react-toastify'
import { appPrivateRoutes } from '../routes'
@ -11,17 +16,12 @@ import { appPrivateRoutes } from '../routes'
* @returns The URL of the uploaded file.
*/
export const uploadToFileStorage = async (
blob: Blob,
file: File,
nostrController: NostrController
) => {
// Get the current timestamp in seconds
const unixNow = Math.floor(Date.now() / 1000)
// Create a File object with the Blob data
const file = new File([blob], `compressed-${unixNow}.sigit`, {
type: 'application/sigit'
})
// Define event metadata for authorization
const event: EventTemplate = {
kind: 24242,
@ -56,7 +56,6 @@ export const uploadToFileStorage = async (
/**
* Sends a Direct Message (DM) to a recipient, encrypting the content and handling authentication.
* @param fileUrl The URL of the encrypted zip file to be included in the DM.
* @param encryptionKey The encryption key used to decrypt the zip file to be included in the DM.
* @param pubkey The public key of the recipient.
* @param nostrController The NostrController instance for handling authentication and encryption.
* @param isSigner Boolean indicating whether the recipient is a signer or viewer.
@ -64,7 +63,6 @@ export const uploadToFileStorage = async (
*/
export const sendDM = async (
fileUrl: string,
encryptionKey: string,
pubkey: string,
nostrController: NostrController,
isSigner: boolean,
@ -77,9 +75,7 @@ export const sendDM = async (
const decryptionUrl = `${window.location.origin}/#${
appPrivateRoutes.sign
}?file=${encodeURIComponent(fileUrl)}&key=${encodeURIComponent(
encryptionKey
)}`
}?file=${encodeURIComponent(fileUrl)}`
const content = `${initialLine}\n\n${decryptionUrl}`
@ -205,3 +201,52 @@ export const signEventForMetaFile = async (
return signedEvent // Return the signed event
}
/**
* Generates the content for keys.json file.
*
* @param users - An array of public keys.
* @param key - The key that will be encrypted for each user.
* @returns A promise that resolves to a JSON string containing the sender's public key and encrypted keys, or null if an error occurs.
*/
export const generateKeysFile = async (
users: string[],
key: string
): Promise<string | null> => {
// Generate a random private key to act as the sender
const privateKey = generateSecretKey()
// Calculate the required length to be a multiple of 10
const requiredLength = Math.ceil(users.length / 10) * 10
const additionalKeysCount = requiredLength - users.length
if (additionalKeysCount > 0) {
// generate random public keys to make the keys array multiple of 10
const additionalPubkeys = Array.from({ length: additionalKeysCount }, () =>
getPublicKey(generateSecretKey())
)
users.push(...additionalPubkeys)
}
// Encrypt the key for each user's public key
const promises = users.map((pubkey) => nip04.encrypt(privateKey, pubkey, key))
// Wait for all encryption promises to resolve
const keys = await Promise.all(promises).catch((err) => {
console.log('Error while generating keys :>> ', err)
toast.error(err.message || 'An error occurred while generating key')
return null
})
// If any encryption promise failed, return null
if (!keys) return null
try {
// Return a JSON string containing the sender's public key and encrypted keys
return JSON.stringify({ sender: getPublicKey(privateKey), keys })
} catch (error) {
// Return null if an error occurs during JSON stringification
return null
}
}