update_260201-3
This commit is contained in:
13
metro.config.js
Normal file
13
metro.config.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const { getDefaultConfig } = require('expo/metro-config');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const config = getDefaultConfig(__dirname);
|
||||||
|
|
||||||
|
config.resolver.extraNodeModules = {
|
||||||
|
...config.resolver.extraNodeModules,
|
||||||
|
crypto: path.resolve(__dirname, 'src/utils/crypto_polyfill.ts'),
|
||||||
|
stream: require.resolve('readable-stream'), // Just in case
|
||||||
|
vm: require.resolve('vm-browserify'),
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
82
package-lock.json
generated
82
package-lock.json
generated
@@ -10,6 +10,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/metro-runtime": "~4.0.1",
|
"@expo/metro-runtime": "~4.0.1",
|
||||||
"@expo/vector-icons": "~14.0.4",
|
"@expo/vector-icons": "~14.0.4",
|
||||||
|
"@noble/ciphers": "^1.3.0",
|
||||||
|
"@noble/hashes": "^1.8.0",
|
||||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||||
"@react-navigation/bottom-tabs": "^6.6.1",
|
"@react-navigation/bottom-tabs": "^6.6.1",
|
||||||
"@react-navigation/native": "^6.1.18",
|
"@react-navigation/native": "^6.1.18",
|
||||||
@@ -19,6 +21,7 @@
|
|||||||
"expo": "~52.0.0",
|
"expo": "~52.0.0",
|
||||||
"expo-asset": "~11.0.5",
|
"expo-asset": "~11.0.5",
|
||||||
"expo-constants": "~17.0.8",
|
"expo-constants": "~17.0.8",
|
||||||
|
"expo-crypto": "~14.0.2",
|
||||||
"expo-font": "~13.0.4",
|
"expo-font": "~13.0.4",
|
||||||
"expo-haptics": "~14.0.0",
|
"expo-haptics": "~14.0.0",
|
||||||
"expo-image-picker": "^17.0.10",
|
"expo-image-picker": "^17.0.10",
|
||||||
@@ -32,7 +35,9 @@
|
|||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "~4.4.0",
|
"react-native-screens": "~4.4.0",
|
||||||
"react-native-view-shot": "^3.8.0",
|
"react-native-view-shot": "^3.8.0",
|
||||||
"react-native-web": "~0.19.13"
|
"react-native-web": "~0.19.13",
|
||||||
|
"readable-stream": "^4.7.0",
|
||||||
|
"vm-browserify": "^1.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
@@ -3211,9 +3216,21 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@noble/ciphers": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@noble/ciphers/-/ciphers-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.21.3 || >=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@noble/hashes": {
|
"node_modules/@noble/hashes": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -5579,6 +5596,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/events": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/events/-/events-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/exec-async": {
|
"node_modules/exec-async": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/exec-async/-/exec-async-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/exec-async/-/exec-async-2.2.0.tgz",
|
||||||
@@ -5756,6 +5782,18 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-crypto": {
|
||||||
|
"version": "14.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/expo-crypto/-/expo-crypto-14.0.2.tgz",
|
||||||
|
"integrity": "sha512-WRc9PBpJraJN29VD5Ef7nCecxJmZNyRKcGkNiDQC1nhY5agppzwhqh7zEzNFarE/GqDgSiaDHS8yd5EgFhP9AQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-font": {
|
"node_modules/expo-font": {
|
||||||
"version": "13.0.4",
|
"version": "13.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-13.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-13.0.4.tgz",
|
||||||
@@ -9063,6 +9101,15 @@
|
|||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/process": {
|
||||||
|
"version": "0.11.10",
|
||||||
|
"resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz",
|
||||||
|
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/progress": {
|
"node_modules/progress": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||||
@@ -9534,6 +9581,22 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "4.7.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/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/readline": {
|
"node_modules/readline": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz",
|
||||||
@@ -10234,6 +10297,15 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/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-width": {
|
"node_modules/string-width": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||||
@@ -11011,6 +11083,12 @@
|
|||||||
"integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==",
|
"integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/vm-browserify": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/vm-browserify/-/vm-browserify-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/walker": {
|
"node_modules/walker": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/metro-runtime": "~4.0.1",
|
"@expo/metro-runtime": "~4.0.1",
|
||||||
"@expo/vector-icons": "~14.0.4",
|
"@expo/vector-icons": "~14.0.4",
|
||||||
|
"@noble/ciphers": "^1.3.0",
|
||||||
|
"@noble/hashes": "^1.8.0",
|
||||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||||
"@react-navigation/bottom-tabs": "^6.6.1",
|
"@react-navigation/bottom-tabs": "^6.6.1",
|
||||||
"@react-navigation/native": "^6.1.18",
|
"@react-navigation/native": "^6.1.18",
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
"expo": "~52.0.0",
|
"expo": "~52.0.0",
|
||||||
"expo-asset": "~11.0.5",
|
"expo-asset": "~11.0.5",
|
||||||
"expo-constants": "~17.0.8",
|
"expo-constants": "~17.0.8",
|
||||||
|
"expo-crypto": "~14.0.2",
|
||||||
"expo-font": "~13.0.4",
|
"expo-font": "~13.0.4",
|
||||||
"expo-haptics": "~14.0.0",
|
"expo-haptics": "~14.0.0",
|
||||||
"expo-image-picker": "^17.0.10",
|
"expo-image-picker": "^17.0.10",
|
||||||
@@ -29,11 +32,13 @@
|
|||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-native": "^0.76.9",
|
"react-native": "^0.76.9",
|
||||||
"react-native-gesture-handler": "~2.20.2",
|
"react-native-gesture-handler": "~2.20.2",
|
||||||
"react-native-view-shot": "^3.8.0",
|
|
||||||
"react-native-reanimated": "~3.16.1",
|
"react-native-reanimated": "~3.16.1",
|
||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "~4.4.0",
|
"react-native-screens": "~4.4.0",
|
||||||
"react-native-web": "~0.19.13"
|
"react-native-view-shot": "^3.8.0",
|
||||||
|
"react-native-web": "~0.19.13",
|
||||||
|
"readable-stream": "^4.7.0",
|
||||||
|
"vm-browserify": "^1.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export default function BiometricModal({
|
|||||||
if (visible) {
|
if (visible) {
|
||||||
setIsScanning(false);
|
setIsScanning(false);
|
||||||
scanAnimation.setValue(0);
|
scanAnimation.setValue(0);
|
||||||
|
|
||||||
// Pulse animation
|
// Pulse animation
|
||||||
Animated.loop(
|
Animated.loop(
|
||||||
Animated.sequence([
|
Animated.sequence([
|
||||||
@@ -57,32 +57,30 @@ export default function BiometricModal({
|
|||||||
|
|
||||||
const handleScan = () => {
|
const handleScan = () => {
|
||||||
setIsScanning(true);
|
setIsScanning(true);
|
||||||
|
|
||||||
Animated.loop(
|
Animated.loop(
|
||||||
Animated.sequence([
|
Animated.sequence([
|
||||||
Animated.timing(scanAnimation, {
|
Animated.timing(scanAnimation, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 800,
|
duration: 400,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
Animated.timing(scanAnimation, {
|
Animated.timing(scanAnimation, {
|
||||||
toValue: 0,
|
toValue: 0,
|
||||||
duration: 800,
|
duration: 400,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
{ iterations: 2 }
|
{ iterations: 1 }
|
||||||
).start(() => {
|
).start(() => {
|
||||||
setTimeout(() => {
|
onSuccess();
|
||||||
onSuccess();
|
|
||||||
}, 300);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const backgroundColor = isDark ? colors.vault.cardBackground : colors.white;
|
const backgroundColor = isDark ? colors.vault.cardBackground : colors.white;
|
||||||
const textColor = isDark ? colors.vault.text : colors.nautical.navy;
|
const textColor = isDark ? colors.vault.text : colors.nautical.navy;
|
||||||
const accentColor = isDark ? colors.vault.primary : colors.nautical.teal;
|
const accentColor = isDark ? colors.vault.primary : colors.nautical.teal;
|
||||||
const accentGradient: [string, string] = isDark
|
const accentGradient: [string, string] = isDark
|
||||||
? [colors.vault.primary, colors.vault.secondary]
|
? [colors.vault.primary, colors.vault.secondary]
|
||||||
: [colors.nautical.teal, colors.nautical.seafoam];
|
: [colors.nautical.teal, colors.nautical.seafoam];
|
||||||
|
|
||||||
@@ -97,10 +95,10 @@ export default function BiometricModal({
|
|||||||
<View style={[styles.container, { backgroundColor }, shadows.medium]}>
|
<View style={[styles.container, { backgroundColor }, shadows.medium]}>
|
||||||
{/* Ship wheel watermark */}
|
{/* Ship wheel watermark */}
|
||||||
<View style={styles.watermark}>
|
<View style={styles.watermark}>
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="ship-wheel"
|
name="ship-wheel"
|
||||||
size={150}
|
size={150}
|
||||||
color={isDark ? colors.vault.primary : colors.nautical.lightMint}
|
color={isDark ? colors.vault.primary : colors.nautical.lightMint}
|
||||||
style={{ opacity: 0.15 }}
|
style={{ opacity: 0.15 }}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -109,7 +107,7 @@ export default function BiometricModal({
|
|||||||
<Text style={[styles.message, { color: isDark ? colors.vault.textSecondary : colors.nautical.sage }]}>
|
<Text style={[styles.message, { color: isDark ? colors.vault.textSecondary : colors.nautical.sage }]}>
|
||||||
{message}
|
{message}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.fingerprintButton}
|
style={styles.fingerprintButton}
|
||||||
onPress={handleScan}
|
onPress={handleScan}
|
||||||
@@ -147,10 +145,10 @@ export default function BiometricModal({
|
|||||||
colors={accentGradient}
|
colors={accentGradient}
|
||||||
style={styles.fingerprintGradient}
|
style={styles.fingerprintGradient}
|
||||||
>
|
>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={isScanning ? "finger-print" : "finger-print-outline"}
|
name={isScanning ? "finger-print" : "finger-print-outline"}
|
||||||
size={48}
|
size={48}
|
||||||
color="#fff"
|
color="#fff"
|
||||||
/>
|
/>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|||||||
@@ -91,12 +91,18 @@ export function getVaultStorageKeys(userId: number | string | null): {
|
|||||||
INITIALIZED: string;
|
INITIALIZED: string;
|
||||||
SHARE_DEVICE: string;
|
SHARE_DEVICE: string;
|
||||||
MNEMONIC_PART_LOCAL: string;
|
MNEMONIC_PART_LOCAL: string;
|
||||||
|
AES_KEY: string;
|
||||||
|
SHARE_SERVER: string;
|
||||||
|
SHARE_HEIR: string;
|
||||||
} {
|
} {
|
||||||
const suffix = userId != null ? `_u${userId}` : '_guest';
|
const suffix = userId != null ? `_u${userId}` : '_guest';
|
||||||
return {
|
return {
|
||||||
INITIALIZED: `${VAULT_KEY_PREFIX}_initialized${suffix}`,
|
INITIALIZED: `${VAULT_KEY_PREFIX}_initialized${suffix}`,
|
||||||
SHARE_DEVICE: `${VAULT_KEY_PREFIX}_s0${suffix}`,
|
SHARE_DEVICE: `${VAULT_KEY_PREFIX}_s0${suffix}`,
|
||||||
MNEMONIC_PART_LOCAL: `sentinel_mnemonic_part_local${suffix}`,
|
MNEMONIC_PART_LOCAL: `sentinel_mnemonic_part_local${suffix}`,
|
||||||
|
AES_KEY: `sentinel_aes_key${suffix}`,
|
||||||
|
SHARE_SERVER: `sentinel_share_server${suffix}`,
|
||||||
|
SHARE_HEIR: `sentinel_share_heir${suffix}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,11 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import * as bip39 from 'bip39';
|
import * as bip39 from 'bip39';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { assetsService } from '../services/assets.service';
|
import { assetsService } from '../services/assets.service';
|
||||||
import { createAssetPayload } from '../services/vault.service';
|
import { getVaultStorageKeys, DEBUG_MODE } from '../config';
|
||||||
|
import { SentinelVault } from '../utils/crypto_core';
|
||||||
import {
|
import {
|
||||||
initialVaultAssets,
|
initialVaultAssets,
|
||||||
mapApiAssetsToVaultAssets,
|
mapApiAssetsToVaultAssets,
|
||||||
@@ -51,7 +53,7 @@ export interface UseVaultAssetsReturn {
|
|||||||
* Vault assets list + create. Fetches on unlock when token exists; keeps mock on error.
|
* Vault assets list + create. Fetches on unlock when token exists; keeps mock on error.
|
||||||
*/
|
*/
|
||||||
export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
|
export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
|
||||||
const { token, signOut } = useAuth();
|
const { user, token, signOut } = useAuth();
|
||||||
const [assets, setAssets] = useState<VaultAsset[]>(initialVaultAssets);
|
const [assets, setAssets] = useState<VaultAsset[]>(initialVaultAssets);
|
||||||
const [isSealing, setIsSealing] = useState(false);
|
const [isSealing, setIsSealing] = useState(false);
|
||||||
const [createError, setCreateError] = useState<string | null>(null);
|
const [createError, setCreateError] = useState<string | null>(null);
|
||||||
@@ -101,19 +103,37 @@ export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
|
|||||||
setIsSealing(true);
|
setIsSealing(true);
|
||||||
setCreateError(null);
|
setCreateError(null);
|
||||||
try {
|
try {
|
||||||
const wordList = bip39.wordlists.english;
|
const vaultKeys = getVaultStorageKeys(user?.id ?? null);
|
||||||
const payload = await createAssetPayload(
|
const [s1Str, aesKeyHex, s0Str, s2Str] = await Promise.all([
|
||||||
title.trim(),
|
AsyncStorage.getItem(vaultKeys.SHARE_SERVER),
|
||||||
content.trim(),
|
AsyncStorage.getItem(vaultKeys.AES_KEY),
|
||||||
wordList,
|
AsyncStorage.getItem(vaultKeys.SHARE_DEVICE),
|
||||||
'note',
|
AsyncStorage.getItem(vaultKeys.SHARE_HEIR),
|
||||||
0
|
]);
|
||||||
);
|
|
||||||
|
if (!s1Str || !aesKeyHex) {
|
||||||
|
throw new Error('Vault keys missing. Please re-unlock your vault.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const vault = new SentinelVault();
|
||||||
|
const aesKey = Buffer.from(aesKeyHex, 'hex');
|
||||||
|
const encryptedBuffer = vault.encryptData(aesKey, content.trim());
|
||||||
|
const content_inner_encrypted = encryptedBuffer.toString('hex');
|
||||||
|
|
||||||
|
if (DEBUG_MODE) {
|
||||||
|
console.log('[DEBUG] Crypto Data during Asset Creation:');
|
||||||
|
console.log(' s0 (Device):', s0Str);
|
||||||
|
console.log(' s1 (Server):', s1Str);
|
||||||
|
console.log(' s2 (Heir): ', s2Str);
|
||||||
|
console.log(' AES Key: ', aesKeyHex);
|
||||||
|
console.log(' Encrypted: ', content_inner_encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
await assetsService.createAsset(
|
await assetsService.createAsset(
|
||||||
{
|
{
|
||||||
title: payload.title,
|
title: title.trim(),
|
||||||
private_key_shard: payload.private_key_shard,
|
private_key_shard: s1Str,
|
||||||
content_inner_encrypted: payload.content_inner_encrypted,
|
content_inner_encrypted,
|
||||||
},
|
},
|
||||||
token
|
token
|
||||||
);
|
);
|
||||||
@@ -143,7 +163,7 @@ export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
|
|||||||
setIsSealing(false);
|
setIsSealing(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[token, refreshAssets, signOut]
|
[token, user, refreshAssets, signOut]
|
||||||
);
|
);
|
||||||
|
|
||||||
const clearCreateError = useCallback(() => setCreateError(null), []);
|
const clearCreateError = useCallback(() => setCreateError(null), []);
|
||||||
|
|||||||
@@ -29,10 +29,14 @@ import { LinearGradient } from 'expo-linear-gradient';
|
|||||||
import { Ionicons, Feather, FontAwesome5 } from '@expo/vector-icons';
|
import { Ionicons, Feather, FontAwesome5 } from '@expo/vector-icons';
|
||||||
import * as ImagePicker from 'expo-image-picker';
|
import * as ImagePicker from 'expo-image-picker';
|
||||||
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
||||||
import { aiService } from '../services/ai.service';
|
import { aiService, AIMessage } from '../services/ai.service';
|
||||||
|
import { assetsService } from '../services/assets.service';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { AI_CONFIG } from '../config';
|
import { AI_CONFIG, getVaultStorageKeys } from '../config';
|
||||||
import { storageService } from '../services/storage.service';
|
import { storageService } from '../services/storage.service';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import { SentinelVault } from '../utils/crypto_core';
|
||||||
|
import { Buffer } from 'buffer';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Type Definitions
|
// Type Definitions
|
||||||
@@ -78,6 +82,18 @@ export default function FlowScreen() {
|
|||||||
const [showHistoryModal, setShowHistoryModal] = useState(false);
|
const [showHistoryModal, setShowHistoryModal] = useState(false);
|
||||||
const modalSlideAnim = useRef(new Animated.Value(0)).current;
|
const modalSlideAnim = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
// Summary state
|
||||||
|
const [showSummaryConfirmModal, setShowSummaryConfirmModal] = useState(false);
|
||||||
|
const [showSummaryResultModal, setShowSummaryResultModal] = useState(false);
|
||||||
|
const [isSummarizing, setIsSummarizing] = useState(false);
|
||||||
|
const [generatedSummary, setGeneratedSummary] = useState('');
|
||||||
|
|
||||||
|
// Save to Vault state
|
||||||
|
const [showVaultConfirmModal, setShowVaultConfirmModal] = useState(false);
|
||||||
|
const [showSaveResultModal, setShowSaveResultModal] = useState(false);
|
||||||
|
const [saveResult, setSaveResult] = useState<{ success: boolean; message: string }>({ success: true, message: '' });
|
||||||
|
const [isSavingToVault, setIsSavingToVault] = useState(false);
|
||||||
|
|
||||||
const [chatHistory, setChatHistory] = useState<ChatSession[]>([
|
const [chatHistory, setChatHistory] = useState<ChatSession[]>([
|
||||||
// Sample history data
|
// Sample history data
|
||||||
{
|
{
|
||||||
@@ -453,6 +469,107 @@ export default function FlowScreen() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle generating summary for current conversation
|
||||||
|
*/
|
||||||
|
const handleGenerateSummary = async () => {
|
||||||
|
if (messages.length === 0) {
|
||||||
|
Alert.alert('No Messages', 'There are no messages to summarize.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
Alert.alert('Login Required', 'Please login to generate a summary.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowSummaryConfirmModal(false);
|
||||||
|
setIsSummarizing(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Convert messages to AIMessage format
|
||||||
|
const aiMessages: AIMessage[] = messages.map(msg => ({
|
||||||
|
role: msg.role,
|
||||||
|
content: msg.content,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const summary = await aiService.summarizeChat(aiMessages, token);
|
||||||
|
setGeneratedSummary(summary);
|
||||||
|
setShowSummaryResultModal(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to generate summary:', error);
|
||||||
|
Alert.alert('Error', 'Failed to generate summary. Please try again later.');
|
||||||
|
} finally {
|
||||||
|
setIsSummarizing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle saving the generated summary to the vault
|
||||||
|
*/
|
||||||
|
const handleSaveToVault = async () => {
|
||||||
|
if (!generatedSummary || isSavingToVault) return;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
Alert.alert('Login Required', 'Please login to save to vault.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowVaultConfirmModal(false);
|
||||||
|
setIsSavingToVault(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Retrieve vault keys
|
||||||
|
if (!user) {
|
||||||
|
Alert.alert('Error', 'User information not found. Please login again.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const vaultKeys = getVaultStorageKeys(user.id);
|
||||||
|
const shareServer = await AsyncStorage.getItem(vaultKeys.SHARE_SERVER);
|
||||||
|
const aesKeyHex = await AsyncStorage.getItem(vaultKeys.AES_KEY);
|
||||||
|
|
||||||
|
if (!shareServer || !aesKeyHex) {
|
||||||
|
Alert.alert(
|
||||||
|
'Vault Not Initialized',
|
||||||
|
'Your vault is not fully initialized. Please set it up in the Vault tab first.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt summary with AES key
|
||||||
|
const vault = new SentinelVault();
|
||||||
|
const aesKey = Buffer.from(aesKeyHex, 'hex');
|
||||||
|
const encryptedSummary = vault.encryptData(aesKey, generatedSummary).toString('hex');
|
||||||
|
|
||||||
|
// Create asset in backend
|
||||||
|
await assetsService.createAsset({
|
||||||
|
title: `Chat Summary - ${new Date().toLocaleDateString()}`,
|
||||||
|
private_key_shard: shareServer,
|
||||||
|
content_inner_encrypted: encryptedSummary,
|
||||||
|
}, token);
|
||||||
|
|
||||||
|
setSaveResult({ success: true, message: 'Summary encrypted and saved to your vault successfully.' });
|
||||||
|
setShowSaveResultModal(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save to vault:', error);
|
||||||
|
setSaveResult({ success: false, message: 'Failed to save summary to vault. Please try again.' });
|
||||||
|
setShowSaveResultModal(true);
|
||||||
|
} finally {
|
||||||
|
setIsSavingToVault(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle closing all summary related modals after successful save or manual close of result
|
||||||
|
*/
|
||||||
|
const handleFinishSaveFlow = () => {
|
||||||
|
setShowSaveResultModal(false);
|
||||||
|
if (saveResult.success) {
|
||||||
|
setShowSummaryResultModal(false);
|
||||||
|
setShowVaultConfirmModal(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Helper Functions
|
// Helper Functions
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -596,6 +713,19 @@ export default function FlowScreen() {
|
|||||||
<Ionicons name="chevron-down" size={14} color={colors.flow.textSecondary} />
|
<Ionicons name="chevron-down" size={14} color={colors.flow.textSecondary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* Summary Button */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.historyButton, { marginRight: spacing.sm }]}
|
||||||
|
onPress={() => setShowSummaryConfirmModal(true)}
|
||||||
|
disabled={messages.length === 0 || isSummarizing}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="document-text-outline"
|
||||||
|
size={20}
|
||||||
|
color={messages.length === 0 || isSummarizing ? colors.flow.textSecondary : colors.flow.primary}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* History Button */}
|
{/* History Button */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.historyButton}
|
style={styles.historyButton}
|
||||||
@@ -843,6 +973,212 @@ export default function FlowScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* Summary Confirmation Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={showSummaryConfirmModal}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={() => setShowSummaryConfirmModal(false)}
|
||||||
|
>
|
||||||
|
<TouchableWithoutFeedback onPress={() => setShowSummaryConfirmModal(false)}>
|
||||||
|
<View style={styles.modalOverlay}>
|
||||||
|
<TouchableWithoutFeedback onPress={e => e.stopPropagation()}>
|
||||||
|
<View style={[styles.modalContent, { paddingBottom: spacing.xl }]}>
|
||||||
|
<View style={styles.modalHandle} />
|
||||||
|
<Text style={styles.modalTitle}>Generate Summary</Text>
|
||||||
|
<Text style={[styles.modalSubtitle, { marginVertical: spacing.base }]}>
|
||||||
|
Would you like to generate a summary for the current conversation?
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={styles.modalActions}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.actionButton, styles.cancelButton]}
|
||||||
|
onPress={() => setShowSummaryConfirmModal(false)}
|
||||||
|
>
|
||||||
|
<Text style={styles.cancelButtonText}>No</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.actionButton, styles.confirmButton]}
|
||||||
|
onPress={handleGenerateSummary}
|
||||||
|
>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[colors.nautical.teal, colors.nautical.seafoam]}
|
||||||
|
style={styles.actionButtonGradient}
|
||||||
|
>
|
||||||
|
<Text style={styles.confirmButtonText}>Yes, Generate</Text>
|
||||||
|
</LinearGradient>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Summary Result Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={showSummaryResultModal}
|
||||||
|
transparent
|
||||||
|
animationType="slide"
|
||||||
|
onRequestClose={() => setShowSummaryResultModal(false)}
|
||||||
|
>
|
||||||
|
<TouchableWithoutFeedback onPress={() => setShowSummaryResultModal(false)}>
|
||||||
|
<View style={styles.modalOverlay}>
|
||||||
|
<TouchableWithoutFeedback onPress={e => e.stopPropagation()}>
|
||||||
|
<View style={[styles.modalContent, { maxHeight: '70%' }]}>
|
||||||
|
<View style={styles.modalHandle} />
|
||||||
|
<View style={styles.modalHeader}>
|
||||||
|
<Text style={styles.modalTitle}>Conversation Summary</Text>
|
||||||
|
<TouchableOpacity onPress={() => setShowSummaryResultModal(false)}>
|
||||||
|
<Ionicons name="close" size={24} color={colors.flow.textSecondary} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ScrollView style={styles.summaryContainer} showsVerticalScrollIndicator={false}>
|
||||||
|
<View style={styles.summaryCard}>
|
||||||
|
<Text style={styles.summaryText}>{generatedSummary}</Text>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<View style={styles.summaryActions}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.actionButton, styles.saveToVaultButton]}
|
||||||
|
onPress={() => setShowVaultConfirmModal(true)}
|
||||||
|
disabled={isSavingToVault}
|
||||||
|
>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[colors.nautical.teal, colors.nautical.seafoam]}
|
||||||
|
style={styles.actionButtonGradient}
|
||||||
|
>
|
||||||
|
{isSavingToVault ? (
|
||||||
|
<ActivityIndicator size="small" color="#fff" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Ionicons name="shield-checkmark-outline" size={20} color="#fff" />
|
||||||
|
<Text style={styles.confirmButtonText}>Save to Vault</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</LinearGradient>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.closeButton}
|
||||||
|
onPress={() => setShowSummaryResultModal(false)}
|
||||||
|
>
|
||||||
|
<Text style={styles.closeButtonText}>Done</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Save to Vault Confirmation Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={showVaultConfirmModal}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={() => setShowVaultConfirmModal(false)}
|
||||||
|
>
|
||||||
|
<TouchableWithoutFeedback onPress={() => setShowVaultConfirmModal(false)}>
|
||||||
|
<View style={styles.modalOverlay}>
|
||||||
|
<TouchableWithoutFeedback onPress={e => e.stopPropagation()}>
|
||||||
|
<View style={[styles.modalContent, { paddingBottom: spacing.xl }]}>
|
||||||
|
<View style={styles.modalHandle} />
|
||||||
|
<Text style={styles.modalTitle}>Save to Vault</Text>
|
||||||
|
<Text style={[styles.modalSubtitle, { marginVertical: spacing.base }]}>
|
||||||
|
Would you like to securely save this summary to your digital vault?
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={styles.modalActions}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.actionButton, styles.cancelButton]}
|
||||||
|
onPress={() => setShowVaultConfirmModal(false)}
|
||||||
|
>
|
||||||
|
<Text style={styles.cancelButtonText}>Cancel</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.actionButton, styles.confirmButton]}
|
||||||
|
onPress={handleSaveToVault}
|
||||||
|
>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[colors.nautical.teal, colors.nautical.seafoam]}
|
||||||
|
style={styles.actionButtonGradient}
|
||||||
|
>
|
||||||
|
<Text style={styles.confirmButtonText}>Yes, Save</Text>
|
||||||
|
</LinearGradient>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Save Result Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={showSaveResultModal}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={handleFinishSaveFlow}
|
||||||
|
>
|
||||||
|
<TouchableWithoutFeedback onPress={handleFinishSaveFlow}>
|
||||||
|
<View style={styles.modalOverlay}>
|
||||||
|
<TouchableWithoutFeedback onPress={e => e.stopPropagation()}>
|
||||||
|
<View style={[styles.modalContent, { paddingBottom: spacing.xl, alignItems: 'center' }]}>
|
||||||
|
<View style={styles.modalHandle} />
|
||||||
|
|
||||||
|
<View style={[
|
||||||
|
styles.resultIconContainer,
|
||||||
|
saveResult.success ? styles.successIconBg : styles.errorIconBg
|
||||||
|
]}>
|
||||||
|
<Ionicons
|
||||||
|
name={saveResult.success ? "checkmark-circle" : "alert-circle"}
|
||||||
|
size={64}
|
||||||
|
color={saveResult.success ? colors.nautical.teal : colors.nautical.coral}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.modalTitle}>
|
||||||
|
{saveResult.success ? 'Success!' : 'Oops!'}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={[styles.modalSubtitle, { marginVertical: spacing.base, textAlign: 'center' }]}>
|
||||||
|
{saveResult.message}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.actionButton, styles.confirmButton, { width: '100%' }]}
|
||||||
|
onPress={handleFinishSaveFlow}
|
||||||
|
>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[colors.nautical.teal, colors.nautical.seafoam]}
|
||||||
|
style={styles.actionButtonGradient}
|
||||||
|
>
|
||||||
|
<Text style={styles.confirmButtonText}>Confirm</Text>
|
||||||
|
</LinearGradient>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Summary Loading Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={isSummarizing}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
>
|
||||||
|
<View style={styles.loadingOverlay}>
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
<ActivityIndicator size="large" color={colors.nautical.teal} />
|
||||||
|
<Text style={styles.loadingText}>Generating Summary...</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1281,4 +1617,101 @@ const styles = StyleSheet.create({
|
|||||||
color: colors.flow.textSecondary,
|
color: colors.flow.textSecondary,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Summary Modal styles
|
||||||
|
modalSubtitle: {
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
color: colors.flow.textSecondary,
|
||||||
|
lineHeight: 22,
|
||||||
|
},
|
||||||
|
modalActions: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: spacing.md,
|
||||||
|
marginTop: spacing.base,
|
||||||
|
},
|
||||||
|
actionButton: {
|
||||||
|
flex: 1,
|
||||||
|
height: 50,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
actionButtonGradient: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
cancelButton: {
|
||||||
|
backgroundColor: colors.nautical.paleAqua,
|
||||||
|
},
|
||||||
|
confirmButton: {
|
||||||
|
// Gradient handled in child
|
||||||
|
},
|
||||||
|
cancelButtonText: {
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: colors.flow.textSecondary,
|
||||||
|
},
|
||||||
|
confirmButtonText: {
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
summaryContainer: {
|
||||||
|
marginVertical: spacing.md,
|
||||||
|
},
|
||||||
|
summaryCard: {
|
||||||
|
backgroundColor: colors.nautical.paleAqua + '40', // 25% opacity
|
||||||
|
padding: spacing.md,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.nautical.lightMint,
|
||||||
|
},
|
||||||
|
summaryText: {
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
color: colors.flow.text,
|
||||||
|
lineHeight: 24,
|
||||||
|
},
|
||||||
|
summaryActions: {
|
||||||
|
marginTop: spacing.md,
|
||||||
|
gap: spacing.sm,
|
||||||
|
},
|
||||||
|
saveToVaultButton: {
|
||||||
|
height: 54,
|
||||||
|
},
|
||||||
|
resultIconContainer: {
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
borderRadius: 40,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: spacing.md,
|
||||||
|
},
|
||||||
|
successIconBg: {
|
||||||
|
backgroundColor: colors.nautical.paleAqua,
|
||||||
|
},
|
||||||
|
errorIconBg: {
|
||||||
|
backgroundColor: 'rgba(231, 76, 60, 0.1)', // coral at 10%
|
||||||
|
},
|
||||||
|
loadingOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: 'rgba(26, 58, 74, 0.6)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
loadingContainer: {
|
||||||
|
backgroundColor: colors.flow.cardBackground,
|
||||||
|
padding: spacing.xl,
|
||||||
|
borderRadius: borderRadius.xl,
|
||||||
|
alignItems: 'center',
|
||||||
|
...shadows.soft,
|
||||||
|
gap: spacing.md,
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
color: colors.flow.text,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { useAuth } from '../context/AuthContext';
|
|||||||
import { useVaultAssets } from '../hooks/useVaultAssets';
|
import { useVaultAssets } from '../hooks/useVaultAssets';
|
||||||
import { getVaultStorageKeys } from '../config';
|
import { getVaultStorageKeys } from '../config';
|
||||||
import { mnemonicToEntropy, splitSecret, serializeShare } from '../utils/sss';
|
import { mnemonicToEntropy, splitSecret, serializeShare } from '../utils/sss';
|
||||||
|
import { SentinelVault } from '@/utils/crypto_core';
|
||||||
|
|
||||||
// Asset type configuration with nautical theme
|
// Asset type configuration with nautical theme
|
||||||
const assetTypeConfig: Record<VaultAssetType, { icon: string; iconType: 'ionicons' | 'feather' | 'material' | 'fontawesome5'; label: string }> = {
|
const assetTypeConfig: Record<VaultAssetType, { icon: string; iconType: 'ionicons' | 'feather' | 'material' | 'fontawesome5'; label: string }> = {
|
||||||
@@ -210,7 +211,7 @@ export default function VaultScreen() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isUnlocked) return;
|
if (isUnlocked) return;
|
||||||
const timer = setTimeout(() => setShowBiometric(true), 500);
|
const timer = setTimeout(() => setShowBiometric(true), 100);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [isUnlocked, hasS0]);
|
}, [isUnlocked, hasS0]);
|
||||||
|
|
||||||
@@ -218,7 +219,7 @@ export default function VaultScreen() {
|
|||||||
if (isUnlocked) {
|
if (isUnlocked) {
|
||||||
Animated.timing(fadeAnim, {
|
Animated.timing(fadeAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 600,
|
duration: 200,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
@@ -230,12 +231,12 @@ export default function VaultScreen() {
|
|||||||
Animated.sequence([
|
Animated.sequence([
|
||||||
Animated.timing(pulseAnim, {
|
Animated.timing(pulseAnim, {
|
||||||
toValue: 1.05,
|
toValue: 1.05,
|
||||||
duration: 1500,
|
duration: 500,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
Animated.timing(pulseAnim, {
|
Animated.timing(pulseAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 1500,
|
duration: 500,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
@@ -380,9 +381,19 @@ export default function VaultScreen() {
|
|||||||
const entropy = mnemonicToEntropy(mnemonicWords, wordList);
|
const entropy = mnemonicToEntropy(mnemonicWords, wordList);
|
||||||
const shares = splitSecret(entropy);
|
const shares = splitSecret(entropy);
|
||||||
const s0 = shares[0]; // device share (S0)
|
const s0 = shares[0]; // device share (S0)
|
||||||
|
const s1 = shares[1]; // server share (S1)
|
||||||
|
const s2 = shares[2]; // heir share (S2)
|
||||||
// S0 is stored in AsyncStorage under user-scoped key — app-level storage, not hardware TEE/SE
|
// S0 is stored in AsyncStorage under user-scoped key — app-level storage, not hardware TEE/SE
|
||||||
|
const vault = new SentinelVault()
|
||||||
|
|
||||||
|
const aes_key = await vault.deriveKey(mnemonicWords.join(' '))
|
||||||
|
|
||||||
await AsyncStorage.setItem(vaultKeys.SHARE_DEVICE, serializeShare(s0));
|
await AsyncStorage.setItem(vaultKeys.SHARE_DEVICE, serializeShare(s0));
|
||||||
|
await AsyncStorage.setItem(vaultKeys.SHARE_SERVER, serializeShare(s1));
|
||||||
await AsyncStorage.setItem(vaultKeys.INITIALIZED, '1');
|
await AsyncStorage.setItem(vaultKeys.INITIALIZED, '1');
|
||||||
|
await AsyncStorage.setItem(vaultKeys.AES_KEY, aes_key.toString('hex'));
|
||||||
|
await AsyncStorage.setItem(vaultKeys.SHARE_HEIR, serializeShare(s2));
|
||||||
|
|
||||||
setHasS0(true);
|
setHasS0(true);
|
||||||
setShowMnemonic(false);
|
setShowMnemonic(false);
|
||||||
setShowBiometric(true);
|
setShowBiometric(true);
|
||||||
@@ -576,327 +587,327 @@ export default function VaultScreen() {
|
|||||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
>
|
>
|
||||||
<View ref={mnemonicRef} collapsable={false}>
|
<View ref={mnemonicRef} collapsable={false}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[colors.sentinel.cardBackground, colors.sentinel.backgroundGradientEnd]}
|
colors={[colors.sentinel.cardBackground, colors.sentinel.backgroundGradientEnd]}
|
||||||
style={styles.mnemonicCard}
|
style={styles.mnemonicCard}
|
||||||
>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.mnemonicClose}
|
|
||||||
onPress={() => setShowMnemonic(false)}
|
|
||||||
activeOpacity={0.85}
|
|
||||||
>
|
>
|
||||||
<Ionicons name="close" size={18} color={colors.sentinel.textSecondary} />
|
<TouchableOpacity
|
||||||
</TouchableOpacity>
|
style={styles.mnemonicClose}
|
||||||
<View style={styles.mnemonicHeader}>
|
onPress={() => setShowMnemonic(false)}
|
||||||
<MaterialCommunityIcons name="key-variant" size={22} color={colors.sentinel.primary} />
|
activeOpacity={0.85}
|
||||||
<Text style={styles.mnemonicTitle}>Mnemonic Setup</Text>
|
>
|
||||||
</View>
|
<Ionicons name="close" size={18} color={colors.sentinel.textSecondary} />
|
||||||
{mnemonicStep === 1 ? (
|
</TouchableOpacity>
|
||||||
<>
|
<View style={styles.mnemonicHeader}>
|
||||||
<Text style={styles.mnemonicSubtitle}>
|
<MaterialCommunityIcons name="key-variant" size={22} color={colors.sentinel.primary} />
|
||||||
Review your 12-word mnemonic. Tap any word to replace it.
|
<Text style={styles.mnemonicTitle}>Mnemonic Setup</Text>
|
||||||
</Text>
|
</View>
|
||||||
<View style={styles.wordGrid}>
|
{mnemonicStep === 1 ? (
|
||||||
{mnemonicWords.map((word, index) => (
|
<>
|
||||||
<TouchableOpacity
|
<Text style={styles.mnemonicSubtitle}>
|
||||||
key={`${word}-${index}`}
|
Review your 12-word mnemonic. Tap any word to replace it.
|
||||||
style={[
|
</Text>
|
||||||
styles.wordChip,
|
<View style={styles.wordGrid}>
|
||||||
replaceIndex === index && styles.wordChipSelected,
|
{mnemonicWords.map((word, index) => (
|
||||||
]}
|
<TouchableOpacity
|
||||||
onPress={() => setReplaceIndex(index)}
|
key={`${word}-${index}`}
|
||||||
activeOpacity={0.8}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={[
|
style={[
|
||||||
styles.wordChipIndex,
|
styles.wordChip,
|
||||||
replaceIndex === index && styles.wordChipTextSelected,
|
replaceIndex === index && styles.wordChipSelected,
|
||||||
]}
|
]}
|
||||||
|
onPress={() => setReplaceIndex(index)}
|
||||||
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
{index + 1}
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.wordChipIndex,
|
||||||
|
replaceIndex === index && styles.wordChipTextSelected,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{index + 1}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.wordChipText,
|
||||||
|
replaceIndex === index && styles.wordChipTextSelected,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{word}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
{replaceIndex !== null ? (
|
||||||
|
<View style={styles.replacePanel}>
|
||||||
|
<Text style={styles.replaceTitle}>
|
||||||
|
Replace word {replaceIndex + 1}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<TextInput
|
||||||
style={[
|
style={styles.replaceInput}
|
||||||
styles.wordChipText,
|
value={replaceQuery}
|
||||||
replaceIndex === index && styles.wordChipTextSelected,
|
onChangeText={setReplaceQuery}
|
||||||
]}
|
placeholder="Search a word"
|
||||||
>
|
placeholderTextColor={colors.sentinel.textSecondary}
|
||||||
{word}
|
autoCapitalize="none"
|
||||||
</Text>
|
autoCorrect={false}
|
||||||
</TouchableOpacity>
|
/>
|
||||||
))}
|
<ScrollView style={styles.replaceList} showsVerticalScrollIndicator={false}>
|
||||||
</View>
|
{(replaceQuery
|
||||||
{replaceIndex !== null ? (
|
? bip39.wordlists.english.filter((word) =>
|
||||||
<View style={styles.replacePanel}>
|
|
||||||
<Text style={styles.replaceTitle}>
|
|
||||||
Replace word {replaceIndex + 1}
|
|
||||||
</Text>
|
|
||||||
<TextInput
|
|
||||||
style={styles.replaceInput}
|
|
||||||
value={replaceQuery}
|
|
||||||
onChangeText={setReplaceQuery}
|
|
||||||
placeholder="Search a word"
|
|
||||||
placeholderTextColor={colors.sentinel.textSecondary}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoCorrect={false}
|
|
||||||
/>
|
|
||||||
<ScrollView style={styles.replaceList} showsVerticalScrollIndicator={false}>
|
|
||||||
{(replaceQuery
|
|
||||||
? bip39.wordlists.english.filter((word) =>
|
|
||||||
word.startsWith(replaceQuery.toLowerCase())
|
word.startsWith(replaceQuery.toLowerCase())
|
||||||
)
|
)
|
||||||
: bip39.wordlists.english
|
: bip39.wordlists.english
|
||||||
)
|
)
|
||||||
.slice(0, 24)
|
.slice(0, 24)
|
||||||
.map((word) => (
|
.map((word) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={word}
|
key={word}
|
||||||
style={styles.replaceOption}
|
style={styles.replaceOption}
|
||||||
onPress={() => handleReplaceWord(word)}
|
onPress={() => handleReplaceWord(word)}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<Text style={styles.replaceOptionText}>{word}</Text>
|
<Text style={styles.replaceOptionText}>{word}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.replaceCancel}
|
style={styles.replaceCancel}
|
||||||
onPress={() => setReplaceIndex(null)}
|
onPress={() => setReplaceIndex(null)}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
>
|
>
|
||||||
<Text style={styles.replaceCancelText}>CANCEL</Text>
|
<Text style={styles.replaceCancelText}>CANCEL</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.mnemonicPrimaryButton}
|
style={styles.mnemonicPrimaryButton}
|
||||||
onPress={() => setMnemonicStep(2)}
|
onPress={() => setMnemonicStep(2)}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
>
|
>
|
||||||
<Text style={styles.mnemonicPrimaryText}>NEXT</Text>
|
<Text style={styles.mnemonicPrimaryText}>NEXT</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{mnemonicStep === 2 ? (
|
{mnemonicStep === 2 ? (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.mnemonicSubtitle}>
|
<Text style={styles.mnemonicSubtitle}>
|
||||||
Confirm your 12-word mnemonic.
|
Confirm your 12-word mnemonic.
|
||||||
</Text>
|
|
||||||
<View style={styles.mnemonicBlock}>
|
|
||||||
<Text style={styles.mnemonicBlockText}>
|
|
||||||
{mnemonicWords.join(' ')}
|
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
<View style={styles.mnemonicBlock}>
|
||||||
<TouchableOpacity
|
<Text style={styles.mnemonicBlockText}>
|
||||||
style={styles.mnemonicPrimaryButton}
|
{mnemonicWords.join(' ')}
|
||||||
onPress={() => setMnemonicStep(3)}
|
|
||||||
activeOpacity={0.85}
|
|
||||||
>
|
|
||||||
<Text style={styles.mnemonicPrimaryText}>CONFIRM</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.mnemonicSecondaryButton}
|
|
||||||
onPress={() => setMnemonicStep(1)}
|
|
||||||
activeOpacity={0.85}
|
|
||||||
>
|
|
||||||
<Text style={styles.mnemonicSecondaryText}>EDIT SELECTION</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{mnemonicStep === 3 ? (
|
|
||||||
<>
|
|
||||||
<Text style={styles.mnemonicSubtitle}>
|
|
||||||
Back up your mnemonic before entering the Vault.
|
|
||||||
</Text>
|
|
||||||
<View style={styles.mnemonicBlock}>
|
|
||||||
<Text style={styles.mnemonicBlockText}>
|
|
||||||
{mnemonicWords.join(' ')}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.mnemonicPrimaryButton, isCapturing && styles.mnemonicButtonDisabled]}
|
|
||||||
onPress={handleScreenshot}
|
|
||||||
activeOpacity={0.85}
|
|
||||||
disabled={isCapturing}
|
|
||||||
>
|
|
||||||
<Text style={styles.mnemonicPrimaryText}>
|
|
||||||
{isCapturing ? 'CAPTURING...' : 'PHYSICAL BACKUP (SCREENSHOT)'}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.mnemonicSecondaryButton}
|
|
||||||
onPress={handleEmailBackup}
|
|
||||||
activeOpacity={0.85}
|
|
||||||
>
|
|
||||||
<Text style={styles.mnemonicSecondaryText}>EMAIL BACKUP</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{mnemonicStep === 4 ? (
|
|
||||||
<>
|
|
||||||
<Text style={styles.mnemonicSubtitle}>
|
|
||||||
Finalizing your vault protection.
|
|
||||||
</Text>
|
|
||||||
<View style={styles.progressContainer}>
|
|
||||||
<View style={styles.progressTrack}>
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
styles.progressFill,
|
|
||||||
{
|
|
||||||
width: progressAnim.interpolate({
|
|
||||||
inputRange: [0, 1],
|
|
||||||
outputRange: ['0%', '100%'],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View style={styles.progressSteps}>
|
|
||||||
<Text style={styles.progressText}>
|
|
||||||
{progressIndex === 0 && '1. Your key is being processed'}
|
|
||||||
{progressIndex === 1 && '2. Your key has been split'}
|
|
||||||
{progressIndex === 2 && '3. Part one stored on this device'}
|
|
||||||
{progressIndex === 3 && '4. Part two uploaded to the cloud'}
|
|
||||||
{progressIndex >= 4 && '5. Part three inquiry initiated'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{mnemonicStep === 5 ? (
|
|
||||||
<>
|
|
||||||
{heirStep === 'decision' ? (
|
|
||||||
<>
|
|
||||||
<Text style={styles.mnemonicSubtitle}>
|
|
||||||
Share Part Three with your legacy handler?
|
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
</View>
|
||||||
style={styles.mnemonicPrimaryButton}
|
<TouchableOpacity
|
||||||
onPress={handleHeirYes}
|
style={styles.mnemonicPrimaryButton}
|
||||||
activeOpacity={0.85}
|
onPress={() => setMnemonicStep(3)}
|
||||||
>
|
activeOpacity={0.85}
|
||||||
<Text style={styles.mnemonicPrimaryText}>YES, SEND</Text>
|
>
|
||||||
</TouchableOpacity>
|
<Text style={styles.mnemonicPrimaryText}>CONFIRM</Text>
|
||||||
<TouchableOpacity
|
</TouchableOpacity>
|
||||||
style={styles.mnemonicSecondaryButton}
|
<TouchableOpacity
|
||||||
onPress={handleHeirNo}
|
style={styles.mnemonicSecondaryButton}
|
||||||
activeOpacity={0.85}
|
onPress={() => setMnemonicStep(1)}
|
||||||
>
|
activeOpacity={0.85}
|
||||||
<Text style={styles.mnemonicSecondaryText}>NOT NOW</Text>
|
>
|
||||||
</TouchableOpacity>
|
<Text style={styles.mnemonicSecondaryText}>EDIT SELECTION</Text>
|
||||||
</>
|
</TouchableOpacity>
|
||||||
) : null}
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{heirStep === 'asset' ? (
|
{mnemonicStep === 3 ? (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.mnemonicSubtitle}>
|
<Text style={styles.mnemonicSubtitle}>
|
||||||
Select the vault item to assign.
|
Back up your mnemonic before entering the Vault.
|
||||||
|
</Text>
|
||||||
|
<View style={styles.mnemonicBlock}>
|
||||||
|
<Text style={styles.mnemonicBlockText}>
|
||||||
|
{mnemonicWords.join(' ')}
|
||||||
</Text>
|
</Text>
|
||||||
<ScrollView style={styles.selectorList} showsVerticalScrollIndicator={false}>
|
</View>
|
||||||
{assets.map((asset) => {
|
<TouchableOpacity
|
||||||
const config = assetTypeConfig[asset.type];
|
style={[styles.mnemonicPrimaryButton, isCapturing && styles.mnemonicButtonDisabled]}
|
||||||
return (
|
onPress={handleScreenshot}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
disabled={isCapturing}
|
||||||
|
>
|
||||||
|
<Text style={styles.mnemonicPrimaryText}>
|
||||||
|
{isCapturing ? 'CAPTURING...' : 'PHYSICAL BACKUP (SCREENSHOT)'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.mnemonicSecondaryButton}
|
||||||
|
onPress={handleEmailBackup}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
>
|
||||||
|
<Text style={styles.mnemonicSecondaryText}>EMAIL BACKUP</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{mnemonicStep === 4 ? (
|
||||||
|
<>
|
||||||
|
<Text style={styles.mnemonicSubtitle}>
|
||||||
|
Finalizing your vault protection.
|
||||||
|
</Text>
|
||||||
|
<View style={styles.progressContainer}>
|
||||||
|
<View style={styles.progressTrack}>
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.progressFill,
|
||||||
|
{
|
||||||
|
width: progressAnim.interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: ['0%', '100%'],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={styles.progressSteps}>
|
||||||
|
<Text style={styles.progressText}>
|
||||||
|
{progressIndex === 0 && '1. Your key is being processed'}
|
||||||
|
{progressIndex === 1 && '2. Your key has been split'}
|
||||||
|
{progressIndex === 2 && '3. Part one stored on this device'}
|
||||||
|
{progressIndex === 3 && '4. Part two uploaded to the cloud'}
|
||||||
|
{progressIndex >= 4 && '5. Part three inquiry initiated'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{mnemonicStep === 5 ? (
|
||||||
|
<>
|
||||||
|
{heirStep === 'decision' ? (
|
||||||
|
<>
|
||||||
|
<Text style={styles.mnemonicSubtitle}>
|
||||||
|
Share Part Three with your legacy handler?
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.mnemonicPrimaryButton}
|
||||||
|
onPress={handleHeirYes}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
>
|
||||||
|
<Text style={styles.mnemonicPrimaryText}>YES, SEND</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.mnemonicSecondaryButton}
|
||||||
|
onPress={handleHeirNo}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
>
|
||||||
|
<Text style={styles.mnemonicSecondaryText}>NOT NOW</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{heirStep === 'asset' ? (
|
||||||
|
<>
|
||||||
|
<Text style={styles.mnemonicSubtitle}>
|
||||||
|
Select the vault item to assign.
|
||||||
|
</Text>
|
||||||
|
<ScrollView style={styles.selectorList} showsVerticalScrollIndicator={false}>
|
||||||
|
{assets.map((asset) => {
|
||||||
|
const config = assetTypeConfig[asset.type];
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={asset.id}
|
||||||
|
style={styles.selectorRow}
|
||||||
|
onPress={() => handleSelectHeirAsset(asset)}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<View style={styles.selectorIcon}>
|
||||||
|
{renderAssetTypeIcon(config, 18, colors.vault.primary)}
|
||||||
|
</View>
|
||||||
|
<View style={styles.selectorContent}>
|
||||||
|
<Text style={styles.selectorTitle}>{asset.label}</Text>
|
||||||
|
<Text style={styles.selectorSubtitle}>{config.label}</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ScrollView>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.mnemonicSecondaryButton}
|
||||||
|
onPress={() => setHeirStep('decision')}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
>
|
||||||
|
<Text style={styles.mnemonicSecondaryText}>BACK</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{heirStep === 'heir' ? (
|
||||||
|
<>
|
||||||
|
<Text style={styles.mnemonicSubtitle}>
|
||||||
|
Choose a legacy handler.
|
||||||
|
</Text>
|
||||||
|
<ScrollView style={styles.selectorList} showsVerticalScrollIndicator={false}>
|
||||||
|
{initialHeirs.map((heir) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={asset.id}
|
key={heir.id}
|
||||||
style={styles.selectorRow}
|
style={styles.selectorRow}
|
||||||
onPress={() => handleSelectHeirAsset(asset)}
|
onPress={() => handleSelectHeir(heir)}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<View style={styles.selectorIcon}>
|
<View style={styles.selectorIcon}>
|
||||||
{renderAssetTypeIcon(config, 18, colors.vault.primary)}
|
<Ionicons name="person-circle" size={20} color={colors.sentinel.primary} />
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.selectorContent}>
|
<View style={styles.selectorContent}>
|
||||||
<Text style={styles.selectorTitle}>{asset.label}</Text>
|
<Text style={styles.selectorTitle}>{heir.name}</Text>
|
||||||
<Text style={styles.selectorSubtitle}>{config.label}</Text>
|
<Text style={styles.selectorSubtitle}>{heir.email}</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
))}
|
||||||
})}
|
</ScrollView>
|
||||||
</ScrollView>
|
<TouchableOpacity
|
||||||
<TouchableOpacity
|
style={styles.mnemonicSecondaryButton}
|
||||||
style={styles.mnemonicSecondaryButton}
|
onPress={() => setHeirStep('asset')}
|
||||||
onPress={() => setHeirStep('decision')}
|
activeOpacity={0.85}
|
||||||
activeOpacity={0.85}
|
>
|
||||||
>
|
<Text style={styles.mnemonicSecondaryText}>BACK</Text>
|
||||||
<Text style={styles.mnemonicSecondaryText}>BACK</Text>
|
</TouchableOpacity>
|
||||||
</TouchableOpacity>
|
</>
|
||||||
</>
|
) : null}
|
||||||
) : null}
|
|
||||||
|
|
||||||
{heirStep === 'heir' ? (
|
{heirStep === 'summary' ? (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.mnemonicSubtitle}>
|
<Text style={styles.mnemonicSubtitle}>
|
||||||
Choose a legacy handler.
|
Confirm assignment details.
|
||||||
</Text>
|
</Text>
|
||||||
<ScrollView style={styles.selectorList} showsVerticalScrollIndicator={false}>
|
<View style={styles.summaryCard}>
|
||||||
{initialHeirs.map((heir) => (
|
<Text style={styles.summaryLabel}>Vault Item</Text>
|
||||||
<TouchableOpacity
|
<Text style={styles.summaryValue}>{selectedHeirAsset?.label}</Text>
|
||||||
key={heir.id}
|
<Text style={styles.summaryLabel}>Legacy Handler</Text>
|
||||||
style={styles.selectorRow}
|
<Text style={styles.summaryValue}>{selectedHeir?.name}</Text>
|
||||||
onPress={() => handleSelectHeir(heir)}
|
<Text style={styles.summaryValue}>{selectedHeir?.email}</Text>
|
||||||
activeOpacity={0.8}
|
<Text style={styles.summaryLabel}>Release Tier</Text>
|
||||||
>
|
<Text style={styles.summaryValue}>Tier {selectedHeir?.releaseLevel}</Text>
|
||||||
<View style={styles.selectorIcon}>
|
</View>
|
||||||
<Ionicons name="person-circle" size={20} color={colors.sentinel.primary} />
|
<TouchableOpacity
|
||||||
</View>
|
style={styles.mnemonicPrimaryButton}
|
||||||
<View style={styles.selectorContent}>
|
onPress={handleSubmitAssignment}
|
||||||
<Text style={styles.selectorTitle}>{heir.name}</Text>
|
activeOpacity={0.85}
|
||||||
<Text style={styles.selectorSubtitle}>{heir.email}</Text>
|
>
|
||||||
</View>
|
<Text style={styles.mnemonicPrimaryText}>SUBMIT</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
<TouchableOpacity
|
||||||
</ScrollView>
|
style={styles.mnemonicSecondaryButton}
|
||||||
<TouchableOpacity
|
onPress={() => setHeirStep('heir')}
|
||||||
style={styles.mnemonicSecondaryButton}
|
activeOpacity={0.85}
|
||||||
onPress={() => setHeirStep('asset')}
|
>
|
||||||
activeOpacity={0.85}
|
<Text style={styles.mnemonicSecondaryText}>EDIT</Text>
|
||||||
>
|
</TouchableOpacity>
|
||||||
<Text style={styles.mnemonicSecondaryText}>BACK</Text>
|
</>
|
||||||
</TouchableOpacity>
|
) : null}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
<View style={styles.stepDots}>
|
||||||
{heirStep === 'summary' ? (
|
<View style={[styles.stepDot, mnemonicStep === 1 && styles.stepDotActive]} />
|
||||||
<>
|
<View style={[styles.stepDot, mnemonicStep !== 1 && styles.stepDotActive]} />
|
||||||
<Text style={styles.mnemonicSubtitle}>
|
</View>
|
||||||
Confirm assignment details.
|
</LinearGradient>
|
||||||
</Text>
|
|
||||||
<View style={styles.summaryCard}>
|
|
||||||
<Text style={styles.summaryLabel}>Vault Item</Text>
|
|
||||||
<Text style={styles.summaryValue}>{selectedHeirAsset?.label}</Text>
|
|
||||||
<Text style={styles.summaryLabel}>Legacy Handler</Text>
|
|
||||||
<Text style={styles.summaryValue}>{selectedHeir?.name}</Text>
|
|
||||||
<Text style={styles.summaryValue}>{selectedHeir?.email}</Text>
|
|
||||||
<Text style={styles.summaryLabel}>Release Tier</Text>
|
|
||||||
<Text style={styles.summaryValue}>Tier {selectedHeir?.releaseLevel}</Text>
|
|
||||||
</View>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.mnemonicPrimaryButton}
|
|
||||||
onPress={handleSubmitAssignment}
|
|
||||||
activeOpacity={0.85}
|
|
||||||
>
|
|
||||||
<Text style={styles.mnemonicPrimaryText}>SUBMIT</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.mnemonicSecondaryButton}
|
|
||||||
onPress={() => setHeirStep('heir')}
|
|
||||||
activeOpacity={0.85}
|
|
||||||
>
|
|
||||||
<Text style={styles.mnemonicSecondaryText}>EDIT</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
<View style={styles.stepDots}>
|
|
||||||
<View style={[styles.stepDot, mnemonicStep === 1 && styles.stepDotActive]} />
|
|
||||||
<View style={[styles.stepDot, mnemonicStep !== 1 && styles.stepDotActive]} />
|
|
||||||
</View>
|
|
||||||
</LinearGradient>
|
|
||||||
</View>
|
</View>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -904,60 +915,60 @@ export default function VaultScreen() {
|
|||||||
|
|
||||||
// Lock screen
|
// Lock screen
|
||||||
const lockScreen = (
|
const lockScreen = (
|
||||||
<View style={styles.lockContainer}>
|
<View style={styles.lockContainer}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[colors.vault.backgroundGradientStart, colors.vault.backgroundGradientEnd]}
|
colors={[colors.vault.backgroundGradientStart, colors.vault.backgroundGradientEnd]}
|
||||||
style={styles.lockGradient}
|
style={styles.lockGradient}
|
||||||
>
|
>
|
||||||
<SafeAreaView style={styles.lockSafeArea}>
|
<SafeAreaView style={styles.lockSafeArea}>
|
||||||
<View style={styles.lockContent}>
|
<View style={styles.lockContent}>
|
||||||
<Animated.View style={[styles.lockIconContainer, { transform: [{ scale: pulseAnim }] }]}>
|
<Animated.View style={[styles.lockIconContainer, { transform: [{ scale: pulseAnim }] }]}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[colors.nautical.teal, colors.nautical.deepTeal]}
|
colors={[colors.nautical.teal, colors.nautical.deepTeal]}
|
||||||
style={styles.lockIconGradient}
|
style={styles.lockIconGradient}
|
||||||
>
|
|
||||||
<MaterialCommunityIcons name="treasure-chest" size={64} color={colors.vault.primary} />
|
|
||||||
</LinearGradient>
|
|
||||||
</Animated.View>
|
|
||||||
|
|
||||||
<Text style={styles.lockTitle}>THE DEEP VAULT</Text>
|
|
||||||
<Text style={styles.lockSubtitle}>Where treasures rest in silence</Text>
|
|
||||||
|
|
||||||
<View style={styles.waveContainer}>
|
|
||||||
<MaterialCommunityIcons name="waves" size={48} color={colors.vault.secondary} style={{ opacity: 0.3 }} />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.unlockButton}
|
|
||||||
onPress={hasS0 ? () => setShowBiometric(true) : handleUnlock}
|
|
||||||
activeOpacity={0.8}
|
|
||||||
>
|
>
|
||||||
<LinearGradient
|
<MaterialCommunityIcons name="treasure-chest" size={64} color={colors.vault.primary} />
|
||||||
colors={[colors.vault.primary, colors.vault.secondary]}
|
</LinearGradient>
|
||||||
style={styles.unlockButtonGradient}
|
</Animated.View>
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 0 }}
|
|
||||||
>
|
|
||||||
<Ionicons name={hasS0 ? 'finger-print' : 'key'} size={20} color={colors.vault.background} />
|
|
||||||
<Text style={styles.unlockButtonText}>
|
|
||||||
{hasS0 === true ? "Captain's Verification" : hasS0 === false ? 'Enter Vault' : 'Loading…'}
|
|
||||||
</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</SafeAreaView>
|
|
||||||
</LinearGradient>
|
|
||||||
|
|
||||||
<BiometricModal
|
<Text style={styles.lockTitle}>THE DEEP VAULT</Text>
|
||||||
visible={hasS0 === true && showBiometric}
|
<Text style={styles.lockSubtitle}>Where treasures rest in silence</Text>
|
||||||
onSuccess={handleBiometricSuccess}
|
|
||||||
onCancel={() => setShowBiometric(false)}
|
<View style={styles.waveContainer}>
|
||||||
title="Enter the Vault"
|
<MaterialCommunityIcons name="waves" size={48} color={colors.vault.secondary} style={{ opacity: 0.3 }} />
|
||||||
message="Verify your identity to access your treasures"
|
</View>
|
||||||
isDark
|
|
||||||
/>
|
<TouchableOpacity
|
||||||
</View>
|
style={styles.unlockButton}
|
||||||
);
|
onPress={hasS0 ? () => setShowBiometric(true) : handleUnlock}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[colors.vault.primary, colors.vault.secondary]}
|
||||||
|
style={styles.unlockButtonGradient}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 1, y: 0 }}
|
||||||
|
>
|
||||||
|
<Ionicons name={hasS0 ? 'finger-print' : 'key'} size={20} color={colors.vault.background} />
|
||||||
|
<Text style={styles.unlockButtonText}>
|
||||||
|
{hasS0 === true ? "Captain's Verification" : hasS0 === false ? 'Enter Vault' : 'Loading…'}
|
||||||
|
</Text>
|
||||||
|
</LinearGradient>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
</LinearGradient>
|
||||||
|
|
||||||
|
<BiometricModal
|
||||||
|
visible={hasS0 === true && showBiometric}
|
||||||
|
onSuccess={handleBiometricSuccess}
|
||||||
|
onCancel={() => setShowBiometric(false)}
|
||||||
|
title="Enter the Vault"
|
||||||
|
message="Verify your identity to access your treasures"
|
||||||
|
isDark
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
const vaultScreen = (
|
const vaultScreen = (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
@@ -1003,7 +1014,7 @@ export default function VaultScreen() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Asset List */}
|
{/* Asset List */}
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.assetList}
|
style={styles.assetList}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
contentContainerStyle={styles.assetListContent}
|
contentContainerStyle={styles.assetListContent}
|
||||||
@@ -1123,8 +1134,8 @@ export default function VaultScreen() {
|
|||||||
onChangeText={setNewLabel}
|
onChangeText={setNewLabel}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.modalLabel}>TREASURE TYPE</Text>
|
<Text style={styles.modalLabel}>TREASURE TYPE</Text>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
horizontal
|
horizontal
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
style={styles.typeScroll}
|
style={styles.typeScroll}
|
||||||
contentContainerStyle={styles.typeScrollContent}
|
contentContainerStyle={styles.typeScrollContent}
|
||||||
|
|||||||
@@ -241,4 +241,34 @@ export const aiService = {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Summarize a chat conversation
|
||||||
|
* @param messages - Array of chat messages
|
||||||
|
* @param token - JWT token for authentication
|
||||||
|
* @returns AI summary text
|
||||||
|
*/
|
||||||
|
async summarizeChat(messages: AIMessage[], token?: string): Promise<string> {
|
||||||
|
if (NO_BACKEND_MODE) {
|
||||||
|
logApiDebug('AI Summary', 'Using mock mode');
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve('This is a mock summary of your conversation. You discussed various topics including AI integration and UI design. The main conclusion was to proceed with the proposed implementation plan.');
|
||||||
|
}, AI_CONFIG.MOCK_RESPONSE_DELAY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const historicalMessages = messages.map(msg => ({
|
||||||
|
role: msg.role,
|
||||||
|
content: msg.content,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const summaryPrompt: AIMessage = {
|
||||||
|
role: 'user',
|
||||||
|
content: 'Please provide a concise summary of the conversation above in Chinese (since the user request was in Chinese). Focus on the main topics discussed and any key conclusions or actions mentioned.',
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.chat([...historicalMessages, summaryPrompt], token);
|
||||||
|
return response.choices[0]?.message?.content || 'No summary generated';
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
202
src/utils/crypto_core.ts
Normal file
202
src/utils/crypto_core.ts
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import * as bip39 from 'bip39';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
|
// 定义分片类型:[x坐标, y坐标]
|
||||||
|
export type Share = [bigint, bigint];
|
||||||
|
|
||||||
|
// 定义生成密钥的返回接口
|
||||||
|
export interface VaultKeys {
|
||||||
|
mnemonic: string;
|
||||||
|
entropyHex: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SentinelKeyEngine {
|
||||||
|
// 使用第 13 个梅森素数 (2^521 - 1)
|
||||||
|
// readonly 确保不会被修改
|
||||||
|
private readonly PRIME: bigint = 2n ** 521n - 1n;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. 生成原始 12 助记词 (Master Key)
|
||||||
|
*/
|
||||||
|
public generateVaultKeys(): VaultKeys {
|
||||||
|
// 生成 128 位强度的助记词 (12 个单词)
|
||||||
|
const mnemonic = bip39.generateMnemonic(128);
|
||||||
|
|
||||||
|
// 将助记词转为 16 进制熵 (Hex String)
|
||||||
|
const entropyHex = bip39.mnemonicToEntropy(mnemonic);
|
||||||
|
|
||||||
|
return { mnemonic, entropyHex };
|
||||||
|
}
|
||||||
|
|
||||||
|
public mnemonicToEntropy(mnemonic: string): string {
|
||||||
|
return bip39.mnemonicToEntropy(mnemonic);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2. SSS (3,2) 门限分片逻辑
|
||||||
|
* @param entropyHex - 16进制字符串 (32字符)
|
||||||
|
*/
|
||||||
|
public splitToShares(entropyHex: string): Share[] {
|
||||||
|
// 将 Hex 熵转换为 BigInt
|
||||||
|
const secretInt = BigInt('0x' + entropyHex);
|
||||||
|
|
||||||
|
// 生成随机系数 a,范围 [0, PRIME-1]
|
||||||
|
const a = this.secureRandomBigInt(this.PRIME);
|
||||||
|
|
||||||
|
// 定义函数 f(x) = (S + a * x) % PRIME
|
||||||
|
const f = (x: number): bigint => {
|
||||||
|
const xBi = BigInt(x);
|
||||||
|
return (secretInt + a * xBi) % this.PRIME;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成 3 个分片: x=1, x=2, x=3
|
||||||
|
const share1: Share = [1n, f(1)]; // 手机分片
|
||||||
|
const share2: Share = [2n, f(2)]; // 云端分片
|
||||||
|
const share3: Share = [3n, f(3)]; // 传承卡分片
|
||||||
|
|
||||||
|
return [share1, share2, share3];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3. 恢复逻辑:拉格朗日插值还原
|
||||||
|
* @param shareA - 第一个分片
|
||||||
|
* @param shareB - 第二个分片
|
||||||
|
*/
|
||||||
|
public recoverFromShares(shareA: Share, shareB: Share): string {
|
||||||
|
const [x1, y1] = shareA;
|
||||||
|
const [x2, y2] = shareB;
|
||||||
|
|
||||||
|
// 计算分子: (x2 * y1 - x1 * y2) % PRIME
|
||||||
|
// TS/JS 的 % 运算符对负数返回负数,需修正为正余数
|
||||||
|
let numerator = (x2 * y1 - x1 * y2) % this.PRIME;
|
||||||
|
if (numerator < 0n) numerator += this.PRIME;
|
||||||
|
|
||||||
|
// 计算分母: (x2 - x1)
|
||||||
|
let denominator = (x2 - x1) % this.PRIME;
|
||||||
|
if (denominator < 0n) denominator += this.PRIME;
|
||||||
|
|
||||||
|
// 计算分母的模逆: denominator^-1 mod PRIME
|
||||||
|
// 费马小定理: a^(p-2) = a^-1 (mod p)
|
||||||
|
const invDenominator = this.modPow(denominator, this.PRIME - 2n, this.PRIME);
|
||||||
|
|
||||||
|
// 还原常数项 S
|
||||||
|
const secretInt = (numerator * invDenominator) % this.PRIME;
|
||||||
|
|
||||||
|
// 转回 Hex 字符串
|
||||||
|
let recoveredEntropyHex = secretInt.toString(16);
|
||||||
|
|
||||||
|
// 补齐前导零 (Pad Start)
|
||||||
|
// 128 bit 熵 = 16 字节 = 32 个 Hex 字符
|
||||||
|
// 如果你的熵是 256 bit,这里需要改为 64
|
||||||
|
recoveredEntropyHex = recoveredEntropyHex.padStart(32, '0');
|
||||||
|
|
||||||
|
return bip39.entropyToMnemonic(recoveredEntropyHex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Private Helper Methods ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成小于 limit 的安全随机 BigInt
|
||||||
|
*/
|
||||||
|
private secureRandomBigInt(limit: bigint): bigint {
|
||||||
|
// 计算需要的字节数
|
||||||
|
const bitLength = limit.toString(2).length;
|
||||||
|
const byteLength = Math.ceil(bitLength / 8);
|
||||||
|
|
||||||
|
let randomBi: bigint;
|
||||||
|
do {
|
||||||
|
const buf = crypto.randomBytes(byteLength);
|
||||||
|
randomBi = BigInt('0x' + buf.toString('hex'));
|
||||||
|
// 拒绝采样:确保结果小于 limit
|
||||||
|
} while (randomBi >= limit);
|
||||||
|
|
||||||
|
return randomBi;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模幂运算: (base^exp) % modulus
|
||||||
|
* 用于计算模逆
|
||||||
|
*/
|
||||||
|
private modPow(base: bigint, exp: bigint, modulus: bigint): bigint {
|
||||||
|
let result = 1n;
|
||||||
|
base = base % modulus;
|
||||||
|
while (exp > 0n) {
|
||||||
|
if (exp % 2n === 1n) result = (result * base) % modulus;
|
||||||
|
exp = exp >> 1n; // 相当于除以 2
|
||||||
|
base = (base * base) % modulus;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SentinelVault {
|
||||||
|
private salt: Buffer;
|
||||||
|
|
||||||
|
constructor(salt?: string | Buffer) {
|
||||||
|
// 默认盐值与 Python 版本保持一致
|
||||||
|
this.salt = salt ? Buffer.from(salt) : Buffer.from('Sentinel_Salt_2026');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 PBKDF2 将助记词转换为 AES-256 密钥 (32 bytes)
|
||||||
|
*/
|
||||||
|
public async deriveKey(mnemonicPhrase: string): Promise<Buffer> {
|
||||||
|
// 1. BIP-39 助记词转种子 (遵循 BIP-39 标准)
|
||||||
|
// Python 的 to_seed 默认返回 64 字节种子
|
||||||
|
const seed = await bip39.mnemonicToSeed(mnemonicPhrase);
|
||||||
|
|
||||||
|
// 2. PBKDF2 派生密钥
|
||||||
|
// 注意:PyCryptodome 的 PBKDF2 默认使用 HMAC-SHA1 (如未指定)
|
||||||
|
// 为了确保与 Python 逻辑严格一致,这里使用 'sha1'
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
crypto.pbkdf2(seed, this.salt, 100000, 32, 'sha1', (err, derivedKey) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
resolve(derivedKey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 AES-256-GCM 模式进行加密
|
||||||
|
*/
|
||||||
|
public encryptData(key: Buffer, plaintext: string): Buffer {
|
||||||
|
// GCM 模式推荐 nonce 长度,Python 默认通常为 16 字节
|
||||||
|
const iv = crypto.randomBytes(16);
|
||||||
|
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
||||||
|
|
||||||
|
const ciphertext = Buffer.concat([
|
||||||
|
cipher.update(plaintext, 'utf8'),
|
||||||
|
cipher.final()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 获取 GCM 认证标签 (16 bytes)
|
||||||
|
const tag = cipher.getAuthTag();
|
||||||
|
|
||||||
|
// 拼接结果:Nonce + Tag + Ciphertext
|
||||||
|
return Buffer.concat([iv, tag, ciphertext]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES-256-GCM 解密
|
||||||
|
*/
|
||||||
|
public decryptData(key: Buffer, encryptedBlob: Buffer): string {
|
||||||
|
try {
|
||||||
|
// 切片提取组件
|
||||||
|
const iv = encryptedBlob.subarray(0, 16);
|
||||||
|
const tag = encryptedBlob.subarray(16, 32);
|
||||||
|
const ciphertext = encryptedBlob.subarray(32);
|
||||||
|
|
||||||
|
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
||||||
|
decipher.setAuthTag(tag);
|
||||||
|
|
||||||
|
const decrypted = Buffer.concat([
|
||||||
|
decipher.update(ciphertext),
|
||||||
|
decipher.final()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return decrypted.toString('utf8');
|
||||||
|
} catch (error) {
|
||||||
|
return "【解密失败】:密钥错误或数据被篡改";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
135
src/utils/crypto_polyfill.ts
Normal file
135
src/utils/crypto_polyfill.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import * as ExpoCrypto from 'expo-crypto';
|
||||||
|
import { Buffer } from 'buffer';
|
||||||
|
import { pbkdf2 as noblePbkdf2 } from '@noble/hashes/pbkdf2';
|
||||||
|
import { sha1 } from '@noble/hashes/sha1';
|
||||||
|
import { sha256 } from '@noble/hashes/sha256';
|
||||||
|
import { sha512 } from '@noble/hashes/sha512';
|
||||||
|
import { gcm } from '@noble/ciphers/aes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node.js Crypto Polyfill for React Native
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function randomBytes(size: number): Buffer {
|
||||||
|
const bytes = new Uint8Array(size);
|
||||||
|
ExpoCrypto.getRandomValues(bytes);
|
||||||
|
return Buffer.from(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashMap: Record<string, any> = {
|
||||||
|
sha1,
|
||||||
|
sha256,
|
||||||
|
sha512,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function pbkdf2(
|
||||||
|
password: string | Buffer,
|
||||||
|
salt: string | Buffer,
|
||||||
|
iterations: number,
|
||||||
|
keylen: number,
|
||||||
|
digest: string,
|
||||||
|
callback: (err: Error | null, derivedKey: Buffer) => void
|
||||||
|
): void {
|
||||||
|
try {
|
||||||
|
const passwordBytes = typeof password === 'string' ? Buffer.from(password) : password;
|
||||||
|
const saltBytes = typeof salt === 'string' ? Buffer.from(salt) : salt;
|
||||||
|
const hasher = hashMap[digest.toLowerCase()];
|
||||||
|
|
||||||
|
if (!hasher) {
|
||||||
|
throw new Error(`Unsupported digest: ${digest}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = noblePbkdf2(hasher, passwordBytes, saltBytes, {
|
||||||
|
c: iterations,
|
||||||
|
dkLen: keylen,
|
||||||
|
});
|
||||||
|
|
||||||
|
callback(null, Buffer.from(result));
|
||||||
|
} catch (err) {
|
||||||
|
callback(err as Error, Buffer.alloc(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AES-GCM Implementation
|
||||||
|
class Cipher {
|
||||||
|
private key: Uint8Array;
|
||||||
|
private iv: Uint8Array;
|
||||||
|
private authTag: Buffer | null = null;
|
||||||
|
private aesGcm: any;
|
||||||
|
private buffer: Buffer = Buffer.alloc(0);
|
||||||
|
|
||||||
|
constructor(key: Buffer, iv: Buffer) {
|
||||||
|
this.key = new Uint8Array(key);
|
||||||
|
this.iv = new Uint8Array(iv);
|
||||||
|
// @noble/ciphers/aes gcm takes (key, nonce)
|
||||||
|
this.aesGcm = gcm(this.key, this.iv);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(data: string | Buffer, inputEncoding?: string): Buffer {
|
||||||
|
const input = typeof data === 'string' ? Buffer.from(data, inputEncoding as any) : data;
|
||||||
|
this.buffer = Buffer.concat([this.buffer, input]);
|
||||||
|
return Buffer.alloc(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
final(): Buffer {
|
||||||
|
const result = this.aesGcm.encrypt(this.buffer);
|
||||||
|
// @noble/ciphers returns ciphertext + tag (16 bytes)
|
||||||
|
const tag = result.slice(-16);
|
||||||
|
const ciphertext = result.slice(0, -16);
|
||||||
|
this.authTag = Buffer.from(tag);
|
||||||
|
return Buffer.from(ciphertext);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAuthTag(): Buffer {
|
||||||
|
if (!this.authTag) throw new Error('Ciphers: TAG not available before final()');
|
||||||
|
return this.authTag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Decipher {
|
||||||
|
private key: Uint8Array;
|
||||||
|
private iv: Uint8Array;
|
||||||
|
private tag: Uint8Array | null = null;
|
||||||
|
private aesGcm: any;
|
||||||
|
private buffer: Buffer = Buffer.alloc(0);
|
||||||
|
|
||||||
|
constructor(key: Buffer, iv: Buffer) {
|
||||||
|
this.key = new Uint8Array(key);
|
||||||
|
this.iv = new Uint8Array(iv);
|
||||||
|
this.aesGcm = gcm(this.key, this.iv);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthTag(tag: Buffer): void {
|
||||||
|
this.tag = new Uint8Array(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(data: Buffer): Buffer {
|
||||||
|
this.buffer = Buffer.concat([this.buffer, data]);
|
||||||
|
return Buffer.alloc(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
final(): Buffer {
|
||||||
|
if (!this.tag) throw new Error('Decipher: Auth tag not set');
|
||||||
|
// @noble/ciphers expects ciphertext then tag
|
||||||
|
const full = new Uint8Array(this.buffer.length + this.tag.length);
|
||||||
|
full.set(this.buffer);
|
||||||
|
full.set(this.tag, this.buffer.length);
|
||||||
|
|
||||||
|
const decrypted = this.aesGcm.decrypt(full);
|
||||||
|
return Buffer.from(decrypted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCipheriv(algorithm: string, key: Buffer, iv: Buffer): Cipher {
|
||||||
|
if (algorithm !== 'aes-256-gcm') {
|
||||||
|
throw new Error(`Polyfill only supports aes-256-gcm, got ${algorithm}`);
|
||||||
|
}
|
||||||
|
return new Cipher(key, iv);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDecipheriv(algorithm: string, key: Buffer, iv: Buffer): Decipher {
|
||||||
|
if (algorithm !== 'aes-256-gcm') {
|
||||||
|
throw new Error(`Polyfill only supports aes-256-gcm, got ${algorithm}`);
|
||||||
|
}
|
||||||
|
return new Decipher(key, iv);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user