From cfcce0ec3e5e6261ada17dadc2303f85892467c8 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 11 Apr 2025 14:32:43 +0200 Subject: [PATCH] Add native react argon2id implementation, add SRP polyfill (#771) --- mobile-app/app/(tabs)/index.tsx | 7 +++++ mobile-app/app/_layout.tsx | 3 +++ mobile-app/ios/Podfile.lock | 16 +++++++++++ mobile-app/package-lock.json | 33 ++++++++++++++++++----- mobile-app/package.json | 3 ++- mobile-app/types/react-native-argon2.d.ts | 22 +++++++++++++++ mobile-app/utils/EncryptionUtility.tsx | 32 +++++++++++++--------- mobile-app/utils/SrpUtility.tsx | 12 +++++++-- 8 files changed, 106 insertions(+), 22 deletions(-) create mode 100644 mobile-app/types/react-native-argon2.d.ts diff --git a/mobile-app/app/(tabs)/index.tsx b/mobile-app/app/(tabs)/index.tsx index 5d7e45edb..e21accbaa 100644 --- a/mobile-app/app/(tabs)/index.tsx +++ b/mobile-app/app/(tabs)/index.tsx @@ -1,6 +1,7 @@ import { Image, StyleSheet, Platform, Button, View, FlatList, Text, SafeAreaView, AppState, TextInput, TouchableOpacity, ActivityIndicator } from 'react-native'; import { NativeModules } from 'react-native'; import { useState, useEffect, useRef } from 'react'; +import { Buffer } from 'buffer'; import { ThemedText } from '@/components/ThemedText'; import { ThemedView } from '@/components/ThemedView'; @@ -60,12 +61,16 @@ export default function HomeScreen() { loginResponse.encryptionSettings ); + console.log('passwordHash', passwordHash); + // Convert uint8 array to uppercase hex string which is expected by the server. const passwordHashString = Buffer.from(passwordHash).toString('hex').toUpperCase(); // Get the derived key as base64 string required for decryption. const passwordHashBase64 = Buffer.from(passwordHash).toString('base64'); + console.log('passwordHashString', passwordHashString); + // 2. Validate login with SRP protocol const validationResponse = await srpUtil.validateLogin( credentials.username, @@ -74,6 +79,8 @@ export default function HomeScreen() { loginResponse ); + console.log('validationResponse', validationResponse); + // 3. Handle 2FA if required if (validationResponse.requiresTwoFactor) { // Store login response as we need it for 2FA validation diff --git a/mobile-app/app/_layout.tsx b/mobile-app/app/_layout.tsx index 43b5a7d99..5a63765dd 100644 --- a/mobile-app/app/_layout.tsx +++ b/mobile-app/app/_layout.tsx @@ -5,6 +5,9 @@ import * as SplashScreen from 'expo-splash-screen'; import { StatusBar } from 'expo-status-bar'; import { useEffect } from 'react'; import 'react-native-reanimated'; +// Required for certain modules such as secure-remote-password which relies on crypto.getRandomValues +// and this is not available in react-native without this polyfill +import 'react-native-get-random-values'; import { useColorScheme } from '@/hooks/useColorScheme'; import { LoadingProvider } from '@/context/LoadingContext'; diff --git a/mobile-app/ios/Podfile.lock b/mobile-app/ios/Podfile.lock index 1ee4ad876..5d7603d06 100644 --- a/mobile-app/ios/Podfile.lock +++ b/mobile-app/ios/Podfile.lock @@ -1,5 +1,6 @@ PODS: - boost (1.84.0) + - CatCrypto (0.3.2) - DoubleConversion (1.1.6) - EXConstants (17.0.8): - ExpoModulesCore @@ -1552,6 +1553,8 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - react-native-get-random-values (1.11.0): + - React-Core - react-native-safe-area-context (4.12.0): - DoubleConversion - glog @@ -1911,6 +1914,9 @@ PODS: - React-logger - React-perflogger - React-utils (= 0.76.9) + - RNArgon2 (2.0.1): + - CatCrypto + - React-Core - RNCAsyncStorage (2.1.2): - DoubleConversion - glog @@ -2151,6 +2157,7 @@ DEPENDENCIES: - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) + - react-native-get-random-values (from `../node_modules/react-native-get-random-values`) - 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`) @@ -2180,6 +2187,7 @@ DEPENDENCIES: - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) - ReactCodegen (from `build/generated/ios`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - RNArgon2 (from `../node_modules/react-native-argon2`) - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNReanimated (from `../node_modules/react-native-reanimated`) @@ -2188,6 +2196,7 @@ DEPENDENCIES: SPEC REPOS: trunk: + - CatCrypto - KeychainAccess - SocketRocket @@ -2311,6 +2320,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon" React-microtasksnativemodule: :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" + react-native-get-random-values: + :path: "../node_modules/react-native-get-random-values" react-native-safe-area-context: :path: "../node_modules/react-native-safe-area-context" react-native-webview: @@ -2369,6 +2380,8 @@ EXTERNAL SOURCES: :path: build/generated/ios ReactCommon: :path: "../node_modules/react-native/ReactCommon" + RNArgon2: + :path: "../node_modules/react-native-argon2" RNCAsyncStorage: :path: "../node_modules/@react-native-async-storage/async-storage" RNGestureHandler: @@ -2382,6 +2395,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 + CatCrypto: a477899b6be4954e75be4897e732da098cc0a5a8 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 EXConstants: fcfc75800824ac2d5c592b5bc74130bad17b146b EXJSONUtils: 01fc7492b66c234e395dcffdd5f53439c5c29c93 @@ -2441,6 +2455,7 @@ SPEC CHECKSUMS: React-logger: c4052eb941cca9a097ef01b59543a656dc088559 React-Mapbuffer: 33546a3ebefbccb8770c33a1f8a5554fa96a54de React-microtasksnativemodule: d80ff86c8902872d397d9622f1a97aadcc12cead + react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba react-native-safe-area-context: cd916088cac5300c3266876218377518987b995e react-native-webview: 6b9fc65c1951203a3e958ff3cc0a858d4b6be901 React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678 @@ -2470,6 +2485,7 @@ SPEC CHECKSUMS: React-utils: ed818f19ab445000d6b5c4efa9d462449326cc9f ReactCodegen: f853a20cc9125c5521c8766b4b49375fec20648b ReactCommon: 300d8d9c5cb1a6cd79a67cf5d8f91e4d477195f9 + RNArgon2: 708e188b7a4d4ec8baf62463927c47abef453a94 RNCAsyncStorage: ab0ad9a78ead8b9f143f0238a5aa535777b12e65 RNGestureHandler: fffddeb8af59709c6d8de11b6461a6af63cad532 RNReanimated: 2e5069649cbab2c946652d3b97589b2ae0526220 diff --git a/mobile-app/package-lock.json b/mobile-app/package-lock.json index 5a597d64e..ff1da85d7 100644 --- a/mobile-app/package-lock.json +++ b/mobile-app/package-lock.json @@ -12,7 +12,6 @@ "@react-native-async-storage/async-storage": "^2.1.2", "@react-navigation/bottom-tabs": "^7.2.0", "@react-navigation/native": "^7.0.14", - "argon2-browser": "^1.18.0", "expo": "~52.0.43", "expo-blur": "~14.0.3", "expo-constants": "~17.0.8", @@ -30,7 +29,9 @@ "react": "18.3.1", "react-dom": "18.3.1", "react-native": "0.76.9", + "react-native-argon2": "^2.0.1", "react-native-gesture-handler": "~2.20.2", + "react-native-get-random-values": "^1.11.0", "react-native-reanimated": "~3.16.1", "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.4.0", @@ -4771,12 +4772,6 @@ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "license": "MIT" }, - "node_modules/argon2-browser": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/argon2-browser/-/argon2-browser-1.18.0.tgz", - "integrity": "sha512-ImVAGIItnFnvET1exhsQB7apRztcoC5TnlSqernMJDUjbc/DLq3UEYeXFrLPrlaIl8cVfwnXb6wX2KpFf2zxHw==", - "license": "MIT" - }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -7127,6 +7122,12 @@ "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", "license": "Apache-2.0" }, + "node_modules/fast-base64-decode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz", + "integrity": "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -11882,6 +11883,12 @@ } } }, + "node_modules/react-native-argon2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/react-native-argon2/-/react-native-argon2-2.0.1.tgz", + "integrity": "sha512-/iOi0S+VVgS1gQGtQgL4ZxUVS4gz6Lav3bgIbtNmr9KbOunnBYzP6/yBe/XxkbpXvasHDwdQnuppOH/nuOBn7w==", + "license": "MIT" + }, "node_modules/react-native-gesture-handler": { "version": "2.20.2", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.20.2.tgz", @@ -11898,6 +11905,18 @@ "react-native": "*" } }, + "node_modules/react-native-get-random-values": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/react-native-get-random-values/-/react-native-get-random-values-1.11.0.tgz", + "integrity": "sha512-4BTbDbRmS7iPdhYLRcz3PGFIpFJBwNZg9g42iwa2P6FOv9vZj/xJc678RZXnLNZzd0qd7Q3CCF6Yd+CU2eoXKQ==", + "license": "MIT", + "dependencies": { + "fast-base64-decode": "^1.0.0" + }, + "peerDependencies": { + "react-native": ">=0.56" + } + }, "node_modules/react-native-helmet-async": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/react-native-helmet-async/-/react-native-helmet-async-2.0.4.tgz", diff --git a/mobile-app/package.json b/mobile-app/package.json index bb5c014c6..cd39af3f1 100644 --- a/mobile-app/package.json +++ b/mobile-app/package.json @@ -19,7 +19,6 @@ "@react-native-async-storage/async-storage": "^2.1.2", "@react-navigation/bottom-tabs": "^7.2.0", "@react-navigation/native": "^7.0.14", - "argon2-browser": "^1.18.0", "expo": "~52.0.43", "expo-blur": "~14.0.3", "expo-constants": "~17.0.8", @@ -37,7 +36,9 @@ "react": "18.3.1", "react-dom": "18.3.1", "react-native": "0.76.9", + "react-native-argon2": "^2.0.1", "react-native-gesture-handler": "~2.20.2", + "react-native-get-random-values": "^1.11.0", "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/types/react-native-argon2.d.ts b/mobile-app/types/react-native-argon2.d.ts new file mode 100644 index 000000000..2957bc8d8 --- /dev/null +++ b/mobile-app/types/react-native-argon2.d.ts @@ -0,0 +1,22 @@ +declare module 'react-native-argon2' { + interface Argon2Options { + iterations?: number; + memory?: number; + parallelism?: number; + hashLength?: number; + mode?: 'argon2i' | 'argon2d' | 'argon2id'; + } + + interface Argon2Result { + encodedHash: string; + rawHash: string; + } + + function argon2( + password: string, + salt: string, + options?: Argon2Options + ): Promise; + + export default argon2; +} \ No newline at end of file diff --git a/mobile-app/utils/EncryptionUtility.tsx b/mobile-app/utils/EncryptionUtility.tsx index 421d18b5f..f729d607d 100644 --- a/mobile-app/utils/EncryptionUtility.tsx +++ b/mobile-app/utils/EncryptionUtility.tsx @@ -1,4 +1,4 @@ -import argon2 from 'argon2-browser/dist/argon2-bundled.min.js'; +import argon2 from 'react-native-argon2'; import { Email } from './types/webapi/Email'; import { EncryptionKey } from './types/EncryptionKey'; import { MailboxEmail } from './types/webapi/MailboxEmail'; @@ -27,18 +27,26 @@ class EncryptionUtility { throw new Error('Unsupported encryption type'); } - const hash = await argon2.hash({ - pass: password, - salt: salt, - time: settings.Iterations, - mem: settings.MemorySize, - parallelism: settings.DegreeOfParallelism, - hashLen: 32, - type: 2, // 0 = Argon2d, 1 = Argon2i, 2 = Argon2id - }); + console.log('trying to hash password'); + const result = await argon2( + password, + salt, + { + iterations: settings.Iterations, + memory: settings.MemorySize, + parallelism: settings.DegreeOfParallelism, + hashLength: 32, + mode: 'argon2id' + } + ); + console.log('result', result); - // Return bytes - return hash.hash; + // Convert the hex string to Uint8Array + const bytes = new Uint8Array(32); + for (let i = 0; i < 32; i++) { + bytes[i] = parseInt(result.rawHash.substring(i * 2, i * 2 + 2), 16); + } + return bytes; } catch (error) { console.error('Argon2 hashing failed:', error); throw error; diff --git a/mobile-app/utils/SrpUtility.tsx b/mobile-app/utils/SrpUtility.tsx index 7013ece0f..ecbaf0709 100644 --- a/mobile-app/utils/SrpUtility.tsx +++ b/mobile-app/utils/SrpUtility.tsx @@ -53,12 +53,17 @@ export class SrpUtility { loginResponse: LoginResponse ): Promise { try { + console.log('validateLogin'); // Generate client ephemeral const clientEphemeral = srp.generateEphemeral(); + console.log('clientEphemeral', clientEphemeral); + // Derive private key const privateKey = srp.derivePrivateKey(loginResponse.salt, username, passwordHash); + console.log('privateKey', privateKey); + // Derive session const sessionProof = srp.deriveSession( clientEphemeral.secret, @@ -68,7 +73,9 @@ export class SrpUtility { privateKey ); - const response = await this.webApiService.rawFetch('Login/Validate', { + console.log('sessionProof', sessionProof); + + const response = await this.webApiService.rawFetch('Auth/validate', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -92,6 +99,7 @@ export class SrpUtility { if (error instanceof ApiAuthError) { throw error; } + console.error('validateLogin error', error); throw new ApiAuthError('Could not reach AliasVault server. Please try again later or contact support if the problem persists.'); } } @@ -122,7 +130,7 @@ export class SrpUtility { privateKey ); - const response = await this.webApiService.rawFetch('Login/Validate2Fa', { + const response = await this.webApiService.rawFetch('Auth/validate-2fa', { method: 'POST', headers: { 'Content-Type': 'application/json',