From 38b3b242d995edc29642e164fa233b0999ea0ec0 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Tue, 22 Apr 2025 11:26:19 +0200 Subject: [PATCH] Make RSA decryption for emails work in react native (#771) --- .../src/utils/EncryptionUtility.tsx | 2 + mobile-app/app/(tabs)/emails.tsx | 323 +++++++++++---- mobile-app/app/emails/[id].tsx | 290 +++++++++++++ mobile-app/app/index.tsx | 6 +- mobile-app/ios/Podfile.lock | 36 ++ .../ios/aliasvault.xcodeproj/project.pbxproj | 2 + mobile-app/package-lock.json | 381 +++++++++++++++++- mobile-app/package.json | 3 + mobile-app/utils/EncryptionUtility.tsx | 15 +- 9 files changed, 967 insertions(+), 91 deletions(-) create mode 100644 mobile-app/app/emails/[id].tsx diff --git a/browser-extension/src/utils/EncryptionUtility.tsx b/browser-extension/src/utils/EncryptionUtility.tsx index 5cbf40848..c2e97c2ce 100644 --- a/browser-extension/src/utils/EncryptionUtility.tsx +++ b/browser-extension/src/utils/EncryptionUtility.tsx @@ -122,6 +122,8 @@ class EncryptionUtility { * Generates a new RSA key pair for asymmetric encryption */ public static async generateRsaKeyPair(): Promise<{ publicKey: string, privateKey: string }> { + // TODO: ensure the key pair is generated in the correct format where private key is in expected + // JWK format that the WASM app already outputs. const keyPair = await crypto.subtle.generateKey( { name: "RSA-OAEP", diff --git a/mobile-app/app/(tabs)/emails.tsx b/mobile-app/app/(tabs)/emails.tsx index 7ce515480..327610b50 100644 --- a/mobile-app/app/(tabs)/emails.tsx +++ b/mobile-app/app/(tabs)/emails.tsx @@ -1,102 +1,253 @@ -import { StyleSheet, Image, Platform } from 'react-native'; - -import { Collapsible } from '@/components/Collapsible'; -import { ExternalLink } from '@/components/ExternalLink'; -import ParallaxScrollView from '@/components/ParallaxScrollView'; +import React, { useEffect, useState, useCallback } from 'react'; +import { StyleSheet, View, TouchableOpacity, RefreshControl, ActivityIndicator } from 'react-native'; +import { useRouter } from 'expo-router'; +import { MailboxEmail } from '@/utils/types/webapi/MailboxEmail'; +import { useDb } from '@/context/DbContext'; +import { useWebApi } from '@/context/WebApiContext'; import { ThemedText } from '@/components/ThemedText'; -import { IconSymbol } from '@/components/ui/IconSymbol'; import { TitleContainer } from '@/components/TitleContainer'; +import ParallaxScrollView from '@/components/ParallaxScrollView'; +import { IconSymbol } from '@/components/ui/IconSymbol'; +import { MailboxBulkRequest, MailboxBulkResponse } from '@/utils/types/webapi/MailboxBulk'; +import EncryptionUtility from '@/utils/EncryptionUtility'; -export default function TabTwoScreen() { - return ( - - }> - - This app includes example code to help you get started. - - - This app has two screens:{' '} - app/(tabs)/index.tsx and{' '} - app/(tabs)/explore.tsx - - - The layout file in app/(tabs)/_layout.tsx{' '} - sets up the tab navigator. - - - Learn more - - - - - You can open this project on Android, iOS, and the web. To open the web version, press{' '} - w in the terminal running this project. - - - - - For static images, you can use the @2x and{' '} - @3x suffixes to provide files for - different screen densities - - - Learn more - - - - - Open app/_layout.tsx to see how to load{' '} - - custom fonts such as this one. +// Simple hook for minimum duration loading state +const useMinDurationLoading = (initialState: boolean, minDuration: number): [boolean, (newState: boolean) => void] => { + const [state, setState] = useState(initialState); + const [timer, setTimer] = useState(null); + + const setStateWithMinDuration = useCallback((newState: boolean) => { + if (newState) { + setState(true); + } else { + if (timer) clearTimeout(timer); + const newTimer = setTimeout(() => setState(false), minDuration); + setTimer(newTimer); + } + }, [minDuration, timer]); + + useEffect(() => { + return () => { + if (timer) clearTimeout(timer); + }; + }, [timer]); + + return [state, setStateWithMinDuration]; +}; + +export default function EmailsScreen() { + const router = useRouter(); + const dbContext = useDb(); + const webApi = useWebApi(); + const [error, setError] = useState(null); + const [emails, setEmails] = useState([]); + const [isLoading, setIsLoading] = useMinDurationLoading(true, 100); + const [isRefreshing, setIsRefreshing] = useState(false); + + const loadEmails = useCallback(async () : Promise => { + try { + setError(null); + + if (!dbContext?.sqliteClient) { + return; + } + + // Get unique email addresses from all credentials + const emailAddresses = await dbContext.sqliteClient.getAllEmailAddresses(); + + try { + // For now we only show the latest 50 emails. No pagination. + const data = await webApi.post('EmailBox/bulk', { + addresses: emailAddresses, + page: 1, + pageSize: 50, + }); + + // Decrypt emails locally using private key associated with the email address + const encryptionKeys = await dbContext.sqliteClient.getAllEncryptionKeys(); + + // Decrypt emails locally using public/private key pairs + const decryptedEmails = await EncryptionUtility.decryptEmailList(data.mails, encryptionKeys); + + setEmails(decryptedEmails); + setIsLoading(false); + } catch (error) { + console.error(error); + throw new Error('Failed to load emails'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } + }, [dbContext?.sqliteClient, webApi]); + + useEffect(() => { + loadEmails(); + }, [loadEmails]); + + const onRefresh = useCallback(async () => { + setIsRefreshing(true); + await loadEmails(); + setIsRefreshing(false); + }, [loadEmails]); + + const formatEmailDate = (dateSystem: string): string => { + const now = new Date(); + const emailDate = new Date(dateSystem); + const secondsAgo = Math.floor((now.getTime() - emailDate.getTime()) / 1000); + + if (secondsAgo < 60) { + return 'just now'; + } else if (secondsAgo < 3600) { + const minutes = Math.floor(secondsAgo / 60); + return `${minutes} ${minutes === 1 ? 'min' : 'mins'} ago`; + } else if (secondsAgo < 86400) { + const hours = Math.floor(secondsAgo / 3600); + return `${hours} ${hours === 1 ? 'hr' : 'hrs'} ago`; + } else if (secondsAgo < 172800) { + return 'yesterday'; + } else { + return emailDate.toLocaleDateString('en-GB', { + day: '2-digit', + month: '2-digit' + }); + } + }; + + const renderContent = () => { + if (isLoading) { + return ( + + + + ); + } + + if (error) { + return ( + + Error: {error} + + ); + } + + if (emails.length === 0) { + return ( + + + You have not received any emails at your private email addresses yet. When you receive a new email, it will appear here. + + ); + } + + return emails.map((email) => ( + router.push(`/emails/${email.id}`)} + > + + + {email.subject} + + + {formatEmailDate(email.dateSystem)} + + + + {email.messagePreview} - - Learn more - - - - - This template has light and dark mode support. The{' '} - useColorScheme() hook lets you inspect - what the user's current color scheme is, and so you can adjust UI colors accordingly. - - - Learn more - - - - - This template includes an example of an animated component. The{' '} - components/HelloWave.tsx component uses - the powerful react-native-reanimated{' '} - library to create a waving hand animation. - - {Platform.select({ - ios: ( - - The components/ParallaxScrollView.tsx{' '} - component provides a parallax effect for the header image. - - ), - })} - - + + )); + }; + + return ( + + + } + > + + {renderContent()} + + {isRefreshing && ( + + + + )} + ); } const styles = StyleSheet.create({ + container: { + flex: 1, + }, headerImage: { color: '#808080', bottom: -90, left: -35, position: 'absolute', }, + centerContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 20, + }, + errorText: { + color: '#ef4444', + textAlign: 'center', + }, + emptyText: { + textAlign: 'center', + opacity: 0.7, + }, + emailCard: { + backgroundColor: '#ffffff', + borderRadius: 8, + padding: 16, + marginHorizontal: 16, + marginBottom: 12, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 3, + elevation: 3, + }, + emailHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: 8, + }, + emailSubject: { + flex: 1, + fontSize: 16, + fontWeight: 'bold', + marginRight: 8, + }, + emailDate: { + fontSize: 12, + opacity: 0.6, + }, + emailPreview: { + fontSize: 14, + opacity: 0.8, + }, + refreshIndicator: { + position: 'absolute', + top: 20, + alignSelf: 'center', + }, }); diff --git a/mobile-app/app/emails/[id].tsx b/mobile-app/app/emails/[id].tsx new file mode 100644 index 000000000..292e47792 --- /dev/null +++ b/mobile-app/app/emails/[id].tsx @@ -0,0 +1,290 @@ +import React, { useEffect, useState } from 'react'; +import { StyleSheet, View, ScrollView, TouchableOpacity, ActivityIndicator, Alert, Share } from 'react-native'; +import { useLocalSearchParams, useRouter } from 'expo-router'; +import { Email } from '@/utils/types/webapi/Email'; +import { useDb } from '@/context/DbContext'; +import { useWebApi } from '@/context/WebApiContext'; +import { ThemedText } from '@/components/ThemedText'; +import { IconSymbol } from '@/components/ui/IconSymbol'; +import EncryptionUtility from '@/utils/EncryptionUtility'; +import WebView from 'react-native-webview'; +import * as FileSystem from 'expo-file-system'; +import { EncryptionKey } from '@/utils/types/EncryptionKey'; + +export default function EmailDetailsScreen() { + const { id } = useLocalSearchParams(); + const router = useRouter(); + const dbContext = useDb(); + const webApi = useWebApi(); + const [error, setError] = useState(null); + const [email, setEmail] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + loadEmail(); + }, [id]); + + const loadEmail = async () => { + try { + setIsLoading(true); + setError(null); + + if (!dbContext?.sqliteClient || !id) { + return; + } + + const response = await webApi.get(`Email/${id}`); + + // Decrypt email locally using public/private key pairs + const encryptionKeys = await dbContext.sqliteClient.getAllEncryptionKeys(); + const decryptedEmail = await EncryptionUtility.decryptEmail(response, encryptionKeys); + setEmail(decryptedEmail); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setIsLoading(false); + } + }; + + const handleDelete = async () => { + Alert.alert( + 'Delete Email', + 'Are you sure you want to delete this email?', + [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Delete', + style: 'destructive', + onPress: async () => { + try { + await webApi.delete(`Email/${id}`); + router.back(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to delete email'); + } + }, + }, + ] + ); + }; + + const handleDownloadAttachment = async (attachment: Email['attachments'][0]) => { + try { + // Get the encrypted attachment bytes from the API + const base64EncryptedAttachment = await webApi.downloadBlobAndConvertToBase64( + `Email/${id}/attachments/${attachment.id}` + ); + + if (!dbContext?.sqliteClient || !email) { + setError('Database context or email not available'); + return; + } + + // Get encryption keys for decryption + const encryptionKeys = await dbContext.sqliteClient.getAllEncryptionKeys(); + + // Decrypt the attachment + const decryptedBytes = await EncryptionUtility.decryptAttachment( + base64EncryptedAttachment, + email, + encryptionKeys + ); + + if (!decryptedBytes) { + setError('Failed to decrypt attachment'); + return; + } + + // Save to temporary file + const tempFile = `${FileSystem.cacheDirectory}${attachment.filename}`; + await FileSystem.writeAsStringAsync(tempFile, decryptedBytes, { + encoding: FileSystem.EncodingType.Base64, + }); + + // Share the file using the native share API + await Share.share({ + url: tempFile, + title: attachment.filename, + }); + + // Clean up temp file + await FileSystem.deleteAsync(tempFile); + } catch (err) { + console.error('handleDownloadAttachment error', err); + setError(err instanceof Error ? err.message : 'Failed to download attachment'); + } + }; + + if (isLoading) { + return ( + + + + ); + } + + if (error) { + return ( + + Error: {error} + + ); + } + + if (!email) { + return ( + + Email not found + + ); + } + + return ( + + {/* Header */} + + + {email.subject} + + + + + + + From: {email.fromDisplay} ({email.fromLocal}@{email.fromDomain}) + + + To: {email.toLocal}@{email.toDomain} + + + Date: {new Date(email.dateSystem).toLocaleString()} + + + + + {/* Email Body */} + + {email.messageHtml ? ( + { + if (event.url !== 'about:blank') { + Share.share({ + url: event.url, + }); + } + }} + /> + ) : ( + {email.messagePlain} + )} + + + {/* Attachments */} + {email.attachments && email.attachments.length > 0 && ( + + Attachments + {email.attachments.map((attachment) => ( + handleDownloadAttachment(attachment)} + > + + + {attachment.filename} ({Math.ceil(attachment.filesize / 1024)} KB) + + + ))} + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + }, + centerContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 20, + }, + errorText: { + color: '#ef4444', + textAlign: 'center', + }, + emptyText: { + textAlign: 'center', + opacity: 0.7, + }, + header: { + padding: 16, + borderBottomWidth: 1, + borderBottomColor: '#e5e5e5', + }, + headerTop: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: 12, + }, + subject: { + fontSize: 20, + fontWeight: 'bold', + flex: 1, + marginRight: 16, + }, + deleteButton: { + padding: 8, + }, + metadata: { + marginTop: 8, + }, + metadataText: { + fontSize: 14, + color: '#666', + marginBottom: 4, + }, + body: { + padding: 16, + }, + webView: { + minHeight: 200, + }, + plainText: { + fontSize: 16, + lineHeight: 24, + }, + attachments: { + padding: 16, + borderTopWidth: 1, + borderTopColor: '#e5e5e5', + }, + attachmentsTitle: { + fontSize: 18, + fontWeight: 'bold', + marginBottom: 12, + }, + attachment: { + flexDirection: 'row', + alignItems: 'center', + padding: 12, + backgroundColor: '#f5f5f5', + borderRadius: 8, + marginBottom: 8, + }, + attachmentName: { + marginLeft: 8, + fontSize: 14, + color: '#666', + }, +}); \ No newline at end of file diff --git a/mobile-app/app/index.tsx b/mobile-app/app/index.tsx index aeb42d546..82c43ba10 100644 --- a/mobile-app/app/index.tsx +++ b/mobile-app/app/index.tsx @@ -1,11 +1,11 @@ import { useEffect, useRef, useState } from 'react'; -import { View, ActivityIndicator } from 'react-native'; import { router } from 'expo-router'; import { useAuth } from '@/context/AuthContext'; import { useDb } from '@/context/DbContext'; import { useVaultSync } from '@/hooks/useVaultSync'; import { ThemedView } from '@/components/ThemedView'; import LoadingIndicator from '@/components/LoadingIndicator'; +import { install } from 'react-native-quick-crypto'; export default function InitialLoadingScreen() { const { isInitialized: isAuthInitialized, isLoggedIn } = useAuth(); @@ -17,6 +17,10 @@ export default function InitialLoadingScreen() { const isFullyInitialized = isAuthInitialized && dbInitialized; const requireLoginOrUnlock = isFullyInitialized && (!isLoggedIn || !dbAvailable); + // Install the react-native-quick-crypto library which is used by the EncryptionUtility + // which acts as a drop-in replacement for the subtle crypto API. + install(); + useEffect(() => { async function initialize() { if (hasInitialized.current) { diff --git a/mobile-app/ios/Podfile.lock b/mobile-app/ios/Podfile.lock index 2e1fe5e68..585b31bd6 100644 --- a/mobile-app/ios/Podfile.lock +++ b/mobile-app/ios/Podfile.lock @@ -257,6 +257,8 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - ExpoSharing (13.0.1): + - ExpoModulesCore - ExpoSplashScreen (0.29.22): - ExpoModulesCore - ExpoSQLite (15.1.4): @@ -277,6 +279,7 @@ PODS: - hermes-engine/Pre-built (= 0.76.9) - hermes-engine/Pre-built (0.76.9) - KeychainAccess (4.2.2) + - OpenSSL-Universal (3.3.3001) - RCT-Folly (2024.10.14.00): - boost - DoubleConversion @@ -1559,6 +1562,29 @@ PODS: - React-Core - react-native-get-random-values (1.11.0): - React-Core + - react-native-quick-crypto (0.7.13): + - DoubleConversion + - glog + - hermes-engine + - OpenSSL-Universal + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-safe-area-context (4.12.0): - DoubleConversion - glog @@ -2165,6 +2191,7 @@ DEPENDENCIES: - ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`) - ExpoLinking (from `../node_modules/expo-linking/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core`) + - ExpoSharing (from `../node_modules/expo-sharing/ios`) - ExpoSplashScreen (from `../node_modules/expo-splash-screen/ios`) - ExpoSQLite (from `../node_modules/expo-sqlite/ios`) - ExpoSymbols (from `../node_modules/expo-symbols/ios`) @@ -2210,6 +2237,7 @@ DEPENDENCIES: - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) - react-native-aes-gcm-crypto (from `../node_modules/react-native-aes-gcm-crypto`) - react-native-get-random-values (from `../node_modules/react-native-get-random-values`) + - react-native-quick-crypto (from `../node_modules/react-native-quick-crypto`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-webview (from `../node_modules/react-native-webview`) - React-nativeconfig (from `../node_modules/react-native/ReactCommon`) @@ -2252,6 +2280,7 @@ SPEC REPOS: trunk: - CatCrypto - KeychainAccess + - OpenSSL-Universal - SocketRocket - SQLite.swift @@ -2296,6 +2325,8 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-linking/ios" ExpoModulesCore: :path: "../node_modules/expo-modules-core" + ExpoSharing: + :path: "../node_modules/expo-sharing/ios" ExpoSplashScreen: :path: "../node_modules/expo-splash-screen/ios" ExpoSQLite: @@ -2381,6 +2412,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-aes-gcm-crypto" react-native-get-random-values: :path: "../node_modules/react-native-get-random-values" + react-native-quick-crypto: + :path: "../node_modules/react-native-quick-crypto" react-native-safe-area-context: :path: "../node_modules/react-native-safe-area-context" react-native-webview: @@ -2476,6 +2509,7 @@ SPEC CHECKSUMS: ExpoKeepAwake: b0171a73665bfcefcfcc311742a72a956e6aa680 ExpoLinking: 8d12bee174ba0cdf31239706578e29e74a417402 ExpoModulesCore: c25d77625038b1968ea1afefc719862c0d8dd993 + ExpoSharing: 849a5ce9985c22598c16ec027e32969be8062e8e ExpoSplashScreen: 0f281e3c2ded4757d2309276c682d023c6299c77 ExpoSQLite: 10fceac6748e9e8f010c70733e0e704fa67399ab ExpoSymbols: f3002db15156cd4e505c77b6ea1df5c984db9965 @@ -2488,6 +2522,7 @@ SPEC CHECKSUMS: glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11 KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51 + OpenSSL-Universal: 6082b0bf950e5636fe0d78def171184e2b3899c2 RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17 RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83 RCTRequired: ca91e5dd26b64f577b528044c962baf171c6b716 @@ -2519,6 +2554,7 @@ SPEC CHECKSUMS: React-microtasksnativemodule: d80ff86c8902872d397d9622f1a97aadcc12cead react-native-aes-gcm-crypto: d572dd7a69f31c539bb8309b3a829bfa3bfad244 react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba + react-native-quick-crypto: 361ecda861e23138ce68fea4b820cbc058688fb5 react-native-safe-area-context: cd916088cac5300c3266876218377518987b995e react-native-webview: 6b9fc65c1951203a3e958ff3cc0a858d4b6be901 React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678 diff --git a/mobile-app/ios/aliasvault.xcodeproj/project.pbxproj b/mobile-app/ios/aliasvault.xcodeproj/project.pbxproj index 7fd7377be..07347af65 100644 --- a/mobile-app/ios/aliasvault.xcodeproj/project.pbxproj +++ b/mobile-app/ios/aliasvault.xcodeproj/project.pbxproj @@ -449,11 +449,13 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-AliasVault/Pods-AliasVault-frameworks.sh", "${PODS_XCFRAMEWORKS_BUILD_DIR}/ExpoSQLite/crsqlite.framework/crsqlite", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL", "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/crsqlite.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", ); runOnlyForDeploymentPostprocessing = 0; diff --git a/mobile-app/package-lock.json b/mobile-app/package-lock.json index 5de15e689..7f6ec1a1b 100644 --- a/mobile-app/package-lock.json +++ b/mobile-app/package-lock.json @@ -12,6 +12,7 @@ "@react-native-async-storage/async-storage": "^2.1.2", "@react-navigation/bottom-tabs": "^7.2.0", "@react-navigation/native": "^7.0.14", + "@types/jsrsasign": "^10.5.15", "expo": "~52.0.43", "expo-blur": "~14.0.3", "expo-clipboard": "^7.0.1", @@ -21,6 +22,7 @@ "expo-haptics": "~14.0.1", "expo-linking": "~7.0.5", "expo-router": "~4.0.20", + "expo-sharing": "~13.0.1", "expo-splash-screen": "~0.29.22", "expo-sqlite": "~15.1.4", "expo-status-bar": "~2.0.1", @@ -35,6 +37,7 @@ "react-native-argon2": "^2.0.1", "react-native-gesture-handler": "~2.20.2", "react-native-get-random-values": "^1.11.0", + "react-native-quick-crypto": "^0.7.13", "react-native-reanimated": "~3.16.1", "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.4.0", @@ -2233,6 +2236,30 @@ "dev": true, "license": "MIT" }, + "node_modules/@craftzdog/react-native-buffer": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@craftzdog/react-native-buffer/-/react-native-buffer-6.0.5.tgz", + "integrity": "sha512-Av+YqfwA9e7jhgI9GFE/gTpwl/H+dRRLmZyJPOpKTy107j9Oj7oXlm3/YiMNz+C/CEGqcKAOqnXDLs4OL6AAFw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "ieee754": "^1.2.1", + "react-native-quick-base64": "^2.0.5" + } + }, "node_modules/@egjs/hammerjs": { "version": "2.0.17", "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", @@ -4659,6 +4686,12 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, + "node_modules/@types/jsrsasign": { + "version": "10.5.15", + "resolved": "https://registry.npmjs.org/@types/jsrsasign/-/jsrsasign-10.5.15.tgz", + "integrity": "sha512-3stUTaSRtN09PPzVWR6aySD9gNnuymz+WviNHoTb85dKu+BjaV4uBbWWGykBBJkfwPtcNZVfTn2lbX00U+yhpQ==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.14.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", @@ -5267,6 +5300,21 @@ "node": ">= 4.0.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/babel-core": { "version": "7.0.0-bridge.0", "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", @@ -5737,6 +5785,24 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -5750,6 +5816,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caller-callsite": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", @@ -6587,6 +6669,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -7122,9 +7221,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.8.x" } @@ -7617,6 +7714,15 @@ "node": ">=10" } }, + "node_modules/expo-sharing": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-13.0.1.tgz", + "integrity": "sha512-qych3Nw65wlFcnzE/gRrsdtvmdV0uF4U4qVMZBJYPG90vYyWh2QM9rp1gVu0KWOBc7N8CC2dSVYn4/BXqJy6Xw==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-splash-screen": { "version": "0.29.22", "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.29.22.tgz", @@ -8023,6 +8129,21 @@ "integrity": "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==", "license": "BSD-2-Clause" }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -8332,6 +8453,18 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -8704,6 +8837,22 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -8716,6 +8865,18 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "license": "MIT" }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -8783,6 +8944,24 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -8850,6 +9029,24 @@ "dev": true, "license": "MIT" }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -8859,6 +9056,21 @@ "node": ">=0.10.0" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -12091,6 +12303,15 @@ "node": ">=4.0.0" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.4.49", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", @@ -12172,6 +12393,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -12601,6 +12831,36 @@ "react-native": "*" } }, + "node_modules/react-native-quick-base64": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-native-quick-base64/-/react-native-quick-base64-2.1.2.tgz", + "integrity": "sha512-xghaXpWdB0ji8OwYyo0fWezRroNxiNFCNFpGUIyE7+qc4gA/IGWnysIG5L0MbdoORv8FkTKUvfd6yCUN5R2VFA==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.5.1" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-quick-crypto": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/react-native-quick-crypto/-/react-native-quick-crypto-0.7.13.tgz", + "integrity": "sha512-9EmXvvBW64gt/hUgeDUGl6KHjDVmtEdC/CbmlyLOTNXxvkOhNI/LhLK9ss6jDqRX3tYTM/DL0uk/+kXSTZQeKA==", + "license": "MIT", + "workspaces": [ + ".", + "example" + ], + "dependencies": { + "@craftzdog/react-native-buffer": "^6.0.5", + "events": "^3.3.0", + "readable-stream": "^4.5.2", + "string_decoder": "^1.3.0", + "util": "^0.12.5" + } + }, "node_modules/react-native-reanimated": { "version": "3.16.7", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.16.7.tgz", @@ -12859,6 +13119,46 @@ "react": "^18.3.1" } }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readable-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/readline": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz", @@ -13179,6 +13479,23 @@ ], "license": "MIT" }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -13458,6 +13775,23 @@ "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", "license": "MIT" }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -13809,6 +14143,15 @@ "node": ">=4" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -14785,6 +15128,19 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -15053,6 +15409,27 @@ "node": ">= 8" } }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wonka": { "version": "6.3.5", "resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.5.tgz", diff --git a/mobile-app/package.json b/mobile-app/package.json index 7836f673c..0701cba84 100644 --- a/mobile-app/package.json +++ b/mobile-app/package.json @@ -19,6 +19,7 @@ "@react-native-async-storage/async-storage": "^2.1.2", "@react-navigation/bottom-tabs": "^7.2.0", "@react-navigation/native": "^7.0.14", + "@types/jsrsasign": "^10.5.15", "expo": "~52.0.43", "expo-blur": "~14.0.3", "expo-clipboard": "^7.0.1", @@ -28,6 +29,7 @@ "expo-haptics": "~14.0.1", "expo-linking": "~7.0.5", "expo-router": "~4.0.20", + "expo-sharing": "~13.0.1", "expo-splash-screen": "~0.29.22", "expo-sqlite": "~15.1.4", "expo-status-bar": "~2.0.1", @@ -42,6 +44,7 @@ "react-native-argon2": "^2.0.1", "react-native-gesture-handler": "~2.20.2", "react-native-get-random-values": "^1.11.0", + "react-native-quick-crypto": "^0.7.13", "react-native-reanimated": "~3.16.1", "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.4.0", diff --git a/mobile-app/utils/EncryptionUtility.tsx b/mobile-app/utils/EncryptionUtility.tsx index 592a576cf..9461caf5c 100644 --- a/mobile-app/utils/EncryptionUtility.tsx +++ b/mobile-app/utils/EncryptionUtility.tsx @@ -113,6 +113,8 @@ class EncryptionUtility { * Generates a new RSA key pair for asymmetric encryption */ public static async generateRsaKeyPair(): Promise<{ publicKey: string, privateKey: string }> { +// TODO: ensure the key pair is generated in the correct format where private key is in expected + // JWK format that the WASM app already outputs. const keyPair = await crypto.subtle.generateKey( { name: "RSA-OAEP", @@ -179,7 +181,8 @@ class EncryptionUtility { const cipherBuffer = Uint8Array.from(atob(ciphertext), c => c.charCodeAt(0)); const plaintextBuffer = await crypto.subtle.decrypt( { - name: "RSA-OAEP" + name: "RSA-OAEP", + hash: "SHA-256", }, privateKeyObj, cipherBuffer @@ -188,7 +191,7 @@ class EncryptionUtility { return new Uint8Array(plaintextBuffer); } catch (error) { console.error('RSA decryption failed:', error); - throw new Error(`Failed to decrypt: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw new Error(`Failed to decrypt: ${error.message}`); } } @@ -250,13 +253,21 @@ class EncryptionUtility { throw new Error('Encryption key not found'); } + console.log('email.encryptedSymmetricKey: ', email.encryptedSymmetricKey); + console.log('encryptionKey.PrivateKey: ', encryptionKey.PrivateKey); + // Decrypt symmetric key with asymmetric private key const symmetricKey = await EncryptionUtility.decryptWithPrivateKey( email.encryptedSymmetricKey, encryptionKey.PrivateKey ); + + console.log('Decrypted symmetricKey: ', symmetricKey); + const symmetricKeyBase64 = Buffer.from(symmetricKey).toString('base64'); + console.log('Decrypted symmetricKeyBase64: ', symmetricKeyBase64); + // Create a new object to avoid mutating the original const decryptedEmail = { ...email };