diff --git a/src/components/username.tsx b/src/components/username.tsx index 7ab6d3d..768d1a9 100644 --- a/src/components/username.tsx +++ b/src/components/username.tsx @@ -67,6 +67,7 @@ export const UserComponent = ({ pubkey, name, image }: UserProps) => { const handleClick = (e: React.MouseEvent) => { e.stopPropagation() + // navigate to user's profile navigate(getProfileRoute(pubkey)) } diff --git a/src/controllers/NostrController.ts b/src/controllers/NostrController.ts index 0cbe166..95f4c08 100644 --- a/src/controllers/NostrController.ts +++ b/src/controllers/NostrController.ts @@ -304,95 +304,135 @@ export class NostrController extends EventEmitter { return event } + /** + * Encrypts the given content for the specified receiver using NIP-44 encryption. + * + * @param receiver The public key of the receiver. + * @param content The content to be encrypted. + * @returns The encrypted content as a string. + * @throws Error if the nostr extension does not support NIP-44, if the private key pair is not found, or if the login method is unsupported. + */ nip44Encrypt = async (receiver: string, content: string) => { + // Retrieve the current login method from the application's redux state. const loginMethod = (store.getState().auth as AuthState).loginMethod + // Handle encryption when the login method is via an extension. if (loginMethod === LoginMethods.extension) { const nostr = this.getNostrObject() + // Check if the nostr object supports NIP-44 encryption. if (!nostr.nip44) { throw new Error( `Your nostr extension does not support nip44 encryption & decryption` ) } + // Encrypt the content using NIP-44 provided by the nostr extension. const encrypted = await nostr.nip44.encrypt(receiver, content) return encrypted as string } + // Handle encryption when the login method is via a private key. if (loginMethod === LoginMethods.privateKey) { const keys = (store.getState().auth as AuthState).keyPair + // Check if the private and public key pair is available. if (!keys) { throw new Error( `Login method is ${LoginMethods.privateKey} but private & public key pair is not found.` ) } + // Decode the private key. const { private: nsec } = keys const privateKey = nip19.decode(nsec).data as Uint8Array + // Generate the conversation key using NIP-44 utilities. const nip44ConversationKey = nip44.v2.utils.getConversationKey( privateKey, receiver ) + + // Encrypt the content using the generated conversation key. const encrypted = nip44.v2.encrypt(content, nip44ConversationKey) return encrypted } + // Throw an error if the login method is nsecBunker (not supported). if (loginMethod === LoginMethods.nsecBunker) { throw new Error( `nip44 encryption is not yet supported for login method '${LoginMethods.nsecBunker}'` ) } + // Throw an error if the login method is undefined or unsupported. throw new Error('Login method is undefined') } + /** + * Decrypts the given content from the specified sender using NIP-44 decryption. + * + * @param sender The public key of the sender. + * @param content The encrypted content to be decrypted. + * @returns The decrypted content as a string. + * @throws Error if the nostr extension does not support NIP-44, if the private key pair is not found, or if the login method is unsupported. + */ nip44Decrypt = async (sender: string, content: string) => { + // Retrieve the current login method from the application's redux state. const loginMethod = (store.getState().auth as AuthState).loginMethod + // Handle decryption when the login method is via an extension. if (loginMethod === LoginMethods.extension) { const nostr = this.getNostrObject() + // Check if the nostr object supports NIP-44 decryption. if (!nostr.nip44) { throw new Error( `Your nostr extension does not support nip44 encryption & decryption` ) } + // Decrypt the content using NIP-44 provided by the nostr extension. const decrypted = await nostr.nip44.decrypt(sender, content) return decrypted as string } + // Handle decryption when the login method is via a private key. if (loginMethod === LoginMethods.privateKey) { const keys = (store.getState().auth as AuthState).keyPair + // Check if the private and public key pair is available. if (!keys) { throw new Error( `Login method is ${LoginMethods.privateKey} but private & public key pair is not found.` ) } + // Decode the private key. const { private: nsec } = keys const privateKey = nip19.decode(nsec).data as Uint8Array + // Generate the conversation key using NIP-44 utilities. const nip44ConversationKey = nip44.v2.utils.getConversationKey( privateKey, sender ) + + // Decrypt the content using the generated conversation key. const decrypted = nip44.v2.decrypt(content, nip44ConversationKey) return decrypted } + // Throw an error if the login method is nsecBunker (not supported). if (loginMethod === LoginMethods.nsecBunker) { throw new Error( `nip44 decryption is not yet supported for login method '${LoginMethods.nsecBunker}'` ) } + // Throw an error if the login method is undefined or unsupported. throw new Error('Login method is undefined') } diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 03ee993..f90ee77 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -32,6 +32,7 @@ import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' import { UserComponent } from '../../components/username' import { MetadataController, NostrController } from '../../controllers' +import { appPrivateRoutes } from '../../routes' import { State } from '../../store/rootReducer' import { CreateSignatureEventContent, @@ -42,6 +43,7 @@ import { } from '../../types' import { encryptArrayBuffer, + formatTimestamp, generateEncryptionKey, generateKeys, generateKeysFile, @@ -58,24 +60,6 @@ import { uploadToFileStorage } from '../../utils' import styles from './style.module.scss' -import { appPrivateRoutes } from '../../routes' - -/** - * Helper function to get the current timestamp in YYMMDD:HHMMSS format - */ -const getFormattedTimestamp = () => { - const now = new Date() - const padZero = (num: number) => (num < 10 ? '0' + num : num) - - const year = now.getFullYear() - const month = padZero(now.getMonth() + 1) // Months are zero-indexed - const day = padZero(now.getDate()) - const hours = padZero(now.getHours()) - const minutes = padZero(now.getMinutes()) - const seconds = padZero(now.getSeconds()) - - return `${year}-${month}-${day}_${hours}:${minutes}:${seconds}` -} export const CreatePage = () => { const navigate = useNavigate() @@ -87,7 +71,7 @@ export const CreatePage = () => { const [authUrl, setAuthUrl] = useState() - const [title, setTitle] = useState(`sigit_${getFormattedTimestamp()}`) + const [title, setTitle] = useState(`sigit_${formatTimestamp(Date.now())}`) const [selectedFiles, setSelectedFiles] = useState([]) const [userInput, setUserInput] = useState('') diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index fa5410b..5204ea7 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -42,6 +42,12 @@ enum SignedStatus { export const SignPage = () => { const navigate = useNavigate() const location = useLocation() + + /** + * uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains keys.json + * arrayBuffer will be received in navigation from create page in offline mode + * meta will be received in navigation from create & home page in online mode + */ const { meta: metaInNavState, arrayBuffer: decryptedArrayBuffer, diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 87302d1..9e10ac0 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -44,6 +44,10 @@ export const VerifyPage = () => { ) const location = useLocation() + /** + * uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains meta.json + * meta will be received in navigation from create & home page in online mode + */ const { uploadedZip, meta: metaInNavState } = location.state || {} const [isLoading, setIsLoading] = useState(false) diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 148f75b..a3bb8ca 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -181,10 +181,18 @@ export const generateKeys = async ( return { sender: getPublicKey(privateKey), keys } } +/** + * Function to extract the ZIP URL and encryption key from the provided metadata. + * @param meta - The metadata object containing the create signature and encryption keys. + * @returns A promise that resolves to an object containing the create signature event, + * create signature content, ZIP URL, and decrypted encryption key. + */ export const extractZipUrlAndEncryptionKey = async (meta: Meta) => { + // Parse the create signature event from the metadata const createSignatureEvent = await parseJson( meta.createSignature ).catch((err) => { + // Log and display an error message if parsing fails console.log('err in parsing the createSignature event:>> ', err) toast.error( err.message || 'error occurred in parsing the create signature event' @@ -192,17 +200,21 @@ export const extractZipUrlAndEncryptionKey = async (meta: Meta) => { return null }) + // Return null if the create signature event could not be parsed if (!createSignatureEvent) return null + // Verify the validity of the create signature event const isValidCreateSignature = verifyEvent(createSignatureEvent) if (!isValidCreateSignature) { toast.error('Create signature is invalid') return null } + // Parse the content of the create signature event const createSignatureContent = await parseJson( createSignatureEvent.content ).catch((err) => { + // Log and display an error message if parsing fails console.log(`err in parsing the createSignature event's content :>> `, err) toast.error( `error occurred in parsing the create signature event's content` @@ -210,29 +222,38 @@ export const extractZipUrlAndEncryptionKey = async (meta: Meta) => { return null }) + // Return null if the create signature content could not be parsed if (!createSignatureContent) return null + // Extract the ZIP URL from the create signature content const zipUrl = createSignatureContent.zipUrl + // Retrieve the user's public key from the state const usersPubkey = (store.getState().auth as AuthState).usersPubkey! const usersNpub = hexToNpub(usersPubkey) + // Return null if the metadata does not contain keys if (!meta.keys) return null const { sender, keys } = meta.keys + // Check if the user's public key is in the keys object if (usersNpub in keys) { + // Instantiate the NostrController to decrypt the encryption key const nostrController = NostrController.getInstance() const decrypted = await nostrController .nip04Decrypt(sender, keys[usersNpub]) .catch((err) => { + // Log and display an error message if decryption fails console.log('An error occurred in decrypting encryption key', err) toast.error('An error occurred in decrypting encryption key') return null }) + // Return null if the encryption key could not be decrypted if (!decrypted) return null + // Return the parsed and decrypted data return { createSignatureEvent, createSignatureContent, diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts index 34f7d52..24f5a9f 100644 --- a/src/utils/nostr.ts +++ b/src/utils/nostr.ts @@ -493,8 +493,10 @@ export const updateUsersAppData = async (meta: Meta) => { if (!newBlossomUrl) return null + // insert new blossom url at the start of the array blossomUrls.unshift(newBlossomUrl) + // only keep last 10 blossom urls, delete older ones if (blossomUrls.length > 10) { const filesToDelete = blossomUrls.splice(10) filesToDelete.forEach((url) => { @@ -509,6 +511,7 @@ export const updateUsersAppData = async (meta: Meta) => { const usersPubkey = (store.getState().auth as AuthState).usersPubkey! + // encrypt content for storing in kind 30078 event const nostrController = NostrController.getInstance() const encryptedContent = await nostrController .nip04Encrypt( @@ -624,24 +627,37 @@ const deleteBlossomFile = async (url: string, privateKey: string) => { console.log('response.data :>> ', response.data) } +/** + * Function to upload user application data to the Blossom server. + * @param sigits - An object containing metadata for the user application data. + * @param processedGiftWraps - An array of processed gift wrap IDs. + * @param privateKey - The private key used for encryption. + * @returns A promise that resolves to the URL of the uploaded file. + */ const uploadUserAppDataToBlossom = async ( sigits: { [key: string]: Meta }, processedGiftWraps: string[], privateKey: string ) => { + // Create an object containing the sigits and processed gift wraps const obj = { sigits, processedGiftWraps } + // Convert the object to a JSON string const stringified = JSON.stringify(obj) + // Convert the private key from hex to bytes const secretKey = hexToBytes(privateKey) + // Encrypt the JSON string using the secret key const encrypted = nip44.v2.encrypt( stringified, nip44ConversationKey(secretKey, getPublicKey(secretKey)) ) + // Create a blob from the encrypted data const blob = new Blob([encrypted], { type: 'application/octet-stream' }) + // Create a file from the blob const file = new File([blob], 'encrypted.txt', { type: 'application/octet-stream' }) @@ -659,8 +675,10 @@ const uploadUserAppDataToBlossom = async ( ] } + // Finalize the event with the private key const authEvent = finalizeEvent(event, hexToBytes(privateKey)) + // URL of the file storage service const FILE_STORAGE_URL = 'https://blossom.sigit.io' // Upload the file to the file storage service using Axios @@ -674,22 +692,34 @@ const uploadUserAppDataToBlossom = async ( return response.data.url as string } +/** + * Function to retrieve and decrypt user application data from Blossom server. + * @param url - The URL to fetch the encrypted data from. + * @param privateKey - The private key used for decryption. + * @returns A promise that resolves to the decrypted and parsed user application data. + */ const getUserAppDataFromBlossom = async (url: string, privateKey: string) => { + // Initialize errorCode to track HTTP error codes let errorCode = 0 + // Fetch the encrypted data from the provided URL const encrypted = await axios .get(url, { - responseType: 'blob' + responseType: 'blob' // Expect a blob response }) .then(async (res) => { + // Convert the blob response to a File object const file = new File([res.data], 'encrypted.txt') + // Read the text content from the file const text = await file.text() return text }) .catch((err) => { + // Log and display an error message if the request fails console.error(`error occurred in getting file from ${url}`, err) toast.error(err.message || `error occurred in getting file from ${url}`) + // Set errorCode to the HTTP status code if available if (err.request) { const { status } = err.request errorCode = status @@ -698,6 +728,7 @@ const getUserAppDataFromBlossom = async (url: string, privateKey: string) => { return null }) + // Return a default value if the requested resource is not found (404) if (errorCode === 404) { return { sigits: {}, @@ -705,20 +736,26 @@ const getUserAppDataFromBlossom = async (url: string, privateKey: string) => { } } + // Return null if the encrypted data could not be retrieved if (!encrypted) return null + // Convert the private key from hex to bytes const secret = hexToBytes(privateKey) + // Get the public key corresponding to the private key const pubkey = getPublicKey(secret) + // Decrypt the encrypted data using the secret and public key const decrypted = nip44.v2.decrypt( encrypted, nip44ConversationKey(secret, pubkey) ) + // Parse the decrypted JSON content const parsedContent = await parseJson<{ sigits: { [key: string]: Meta } processedGiftWraps: string[] }>(decrypted).catch((err) => { + // Log and display an error message if parsing fails console.log( 'An error occurred in parsing the user app data content from blossom server', err @@ -729,15 +766,22 @@ const getUserAppDataFromBlossom = async (url: string, privateKey: string) => { return null }) + // Return the parsed content return parsedContent } +/** + * Function to subscribe to sigits notifications for a specified public key. + * @param pubkey - The public key to subscribe to. + * @returns A promise that resolves when the subscription is successful. + */ export const subscribeForSigits = async (pubkey: string) => { - // Get relay list metadata + // Instantiate the MetadataController to retrieve relay list metadata const metadataController = new MetadataController() const relaySet = await metadataController .findRelayListMetadata(pubkey) .catch((err) => { + // Log an error if retrieving relay list metadata fails console.log( `An error occurred while finding relay list metadata for ${hexToNpub(pubkey)}`, err @@ -751,15 +795,20 @@ export const subscribeForSigits = async (pubkey: string) => { // Ensure relay list is not empty if (relaySet.read.length === 0) return + // Define the filter for the subscription const filter: Filter = { kinds: [1059], '#p': [pubkey] } + + // Instantiate a new SimplePool for the subscription const pool = new SimplePool() + // Subscribe to the specified relays with the defined filter return pool.subscribeMany(relaySet.read, [filter], { + // Define a callback function to handle received events onevent: (event) => { - processReceivedEvent(event) + processReceivedEvent(event) // Process the received event } }) } @@ -810,9 +859,16 @@ const processReceivedEvent = async (event: Event, difficulty: number = 5) => { updateUsersAppData(meta) } +/** + * Function to send a notification to a specified receiver. + * @param receiver - The recipient's public key. + * @param meta - Metadata associated with the notification. + */ export const sendNotification = async (receiver: string, meta: Meta) => { + // Retrieve the user's public key from the state const usersPubkey = (store.getState().auth as AuthState).usersPubkey! + // Create an unsigned event object with the provided metadata const unsignedEvent: UnsignedEvent = { kind: 938, pubkey: usersPubkey, @@ -821,13 +877,15 @@ export const sendNotification = async (receiver: string, meta: Meta) => { created_at: now() } + // Wrap the unsigned event with the receiver's information const wrappedEvent = createWrap(unsignedEvent, receiver) - // Get relay list metadata + // Instantiate the MetadataController to retrieve relay list metadata const metadataController = new MetadataController() const relaySet = await metadataController .findRelayListMetadata(receiver) .catch((err) => { + // Log an error if retrieving relay list metadata fails console.log( `An error occurred while finding relay list metadata for ${hexToNpub(receiver)}`, err @@ -845,10 +903,12 @@ export const sendNotification = async (receiver: string, meta: Meta) => { // Publish the notification event to the recipient's read relays const nostrController = NostrController.getInstance() + // Attempt to publish the event to the relays, with a timeout of 2 minutes await Promise.race([ nostrController.publishEvent(wrappedEvent, relaySet.read), timeout(1000 * 60 * 2) ]).catch((err) => { + // Log an error if publishing the notification event fails console.log( `An error occurred while publishing notification event for ${hexToNpub(receiver)}`, err