Add native react argon2id implementation, add SRP polyfill (#771)

This commit is contained in:
Leendert de Borst
2025-04-11 14:32:43 +02:00
parent 1b70c0c727
commit cfcce0ec3e
8 changed files with 106 additions and 22 deletions

View File

@@ -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

View File

@@ -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';

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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<Argon2Result>;
export default argon2;
}

View File

@@ -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;

View File

@@ -53,12 +53,17 @@ export class SrpUtility {
loginResponse: LoginResponse
): Promise<ValidateLoginResponse> {
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',