mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-20 20:49:47 +02:00
improve mobile sync security
This commit is contained in:
+30
-6
@@ -34,6 +34,7 @@ import {
|
||||
setDesktopRemoteMediaSession,
|
||||
subscribeDesktopRemoteMediaSessionCommands,
|
||||
} from '@/services/desktopRemoteMediaSession';
|
||||
import { identityMatchesPinnedConnection } from '@/services/desktopSyncPolicy';
|
||||
import {
|
||||
getDesktopRemoteConnection,
|
||||
setDesktopRemoteConnection,
|
||||
@@ -170,6 +171,8 @@ const DESKTOP_SYNC_REQUEST_POLL_MS = 60_000;
|
||||
function DesktopSyncAutoTrigger() {
|
||||
const connectionState = useDesktopRemoteStore((s) => s.connectionState);
|
||||
const discovered = useDesktopRemoteStore((s) => s.discovered);
|
||||
const desktopSyncHydrated = useDesktopSyncStore((s) => s.hydrated);
|
||||
const desktopSyncEnabled = useDesktopSyncStore((s) => s.desktopSyncEnabled);
|
||||
|
||||
useEffect(() => {
|
||||
void useDesktopSyncStore.getState().hydrate();
|
||||
@@ -182,6 +185,8 @@ function DesktopSyncAutoTrigger() {
|
||||
let startupRetryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
const onActive = () => {
|
||||
void (async () => {
|
||||
const sync = useDesktopSyncStore.getState();
|
||||
if (!sync.hydrated || !sync.desktopSyncEnabled) return;
|
||||
const connection = await getDesktopRemoteConnection();
|
||||
if (!connection) return;
|
||||
useDesktopSyncStore.getState().maybeAutoSync('foreground');
|
||||
@@ -210,31 +215,41 @@ function DesktopSyncAutoTrigger() {
|
||||
subscription.remove();
|
||||
if (burstTimer !== null) clearTimeout(burstTimer);
|
||||
if (startupRetryTimer !== null) clearTimeout(startupRetryTimer);
|
||||
if (!useDesktopSyncStore.getState().desktopSyncEnabled) {
|
||||
void useDesktopRemoteStore.getState().stopDiscovery();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, [desktopSyncEnabled, desktopSyncHydrated]);
|
||||
|
||||
// Desktop-initiated "Sync now" pickup: a cheap identity poll while
|
||||
// foregrounded (the SSE nudge only reaches us while the remote screen's
|
||||
// stream happens to be connected). fetchDesktopRemoteIdentity swallows
|
||||
// errors, so a powered-off desktop costs one timed-out request per minute.
|
||||
useEffect(() => {
|
||||
if (!desktopSyncHydrated || !desktopSyncEnabled) return;
|
||||
const timer = setInterval(() => {
|
||||
if (AppState.currentState !== 'active') return;
|
||||
void (async () => {
|
||||
const sync = useDesktopSyncStore.getState();
|
||||
if (!sync.hydrated || !sync.desktopSyncEnabled) return;
|
||||
const connection = await getDesktopRemoteConnection();
|
||||
if (!connection) return;
|
||||
const identity = await fetchDesktopRemoteIdentity(connection.baseUrl);
|
||||
const identity = await fetchDesktopRemoteIdentity(
|
||||
connection.baseUrl,
|
||||
connection.certificateFingerprint
|
||||
);
|
||||
if (identity?.syncRequestedAt) {
|
||||
useDesktopSyncStore.getState().handleSyncRequest();
|
||||
}
|
||||
})();
|
||||
}, DESKTOP_SYNC_REQUEST_POLL_MS);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
}, [desktopSyncEnabled, desktopSyncHydrated]);
|
||||
|
||||
// Paired desktop spotted on the LAN: refresh a stale baseUrl (DHCP moves)
|
||||
// and trigger a sync.
|
||||
useEffect(() => {
|
||||
if (!desktopSyncHydrated || !desktopSyncEnabled) return;
|
||||
if (discovered.length === 0) return;
|
||||
void (async () => {
|
||||
const connection = await getDesktopRemoteConnection();
|
||||
@@ -242,6 +257,15 @@ function DesktopSyncAutoTrigger() {
|
||||
const match = discovered.find((desktop) => desktop.endpointUuid === connection.endpointUuid);
|
||||
if (!match) return;
|
||||
if (match.baseUrl && match.baseUrl !== connection.baseUrl) {
|
||||
const identity = await fetchDesktopRemoteIdentity(
|
||||
match.baseUrl,
|
||||
connection.certificateFingerprint
|
||||
);
|
||||
if (!identity || !identityMatchesPinnedConnection(
|
||||
connection.endpointUuid,
|
||||
identity.protocolVersion,
|
||||
identity.endpointUuid
|
||||
)) return;
|
||||
const updated = { ...connection, baseUrl: match.baseUrl };
|
||||
await setDesktopRemoteConnection(updated);
|
||||
if (useDesktopRemoteStore.getState().connection) {
|
||||
@@ -250,14 +274,14 @@ function DesktopSyncAutoTrigger() {
|
||||
}
|
||||
useDesktopSyncStore.getState().maybeAutoSync('discovery');
|
||||
})();
|
||||
}, [discovered]);
|
||||
}, [desktopSyncEnabled, desktopSyncHydrated, discovered]);
|
||||
|
||||
// The remote screen connected — the desktop is definitely reachable.
|
||||
useEffect(() => {
|
||||
if (connectionState === 'connected') {
|
||||
if (desktopSyncHydrated && desktopSyncEnabled && connectionState === 'connected') {
|
||||
useDesktopSyncStore.getState().maybeAutoSync('connected');
|
||||
}
|
||||
}, [connectionState]);
|
||||
}, [desktopSyncEnabled, desktopSyncHydrated, connectionState]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
+48
-21
@@ -225,7 +225,20 @@ export default function DesktopRemoteScreen() {
|
||||
const ripple = useRipple();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const { pair } = useLocalSearchParams<{ pair?: string }>();
|
||||
const pairingParams = useLocalSearchParams<{
|
||||
pair?: string;
|
||||
baseUrl?: string;
|
||||
ticket?: string;
|
||||
endpointUuid?: string;
|
||||
fingerprint?: string;
|
||||
protocolVersion?: string;
|
||||
}>();
|
||||
const pairingRoutePair = pairingParams.pair;
|
||||
const pairingRouteBaseUrl = pairingParams.baseUrl;
|
||||
const pairingRouteTicket = pairingParams.ticket;
|
||||
const pairingRouteEndpointUuid = pairingParams.endpointUuid;
|
||||
const pairingRouteFingerprint = pairingParams.fingerprint;
|
||||
const pairingRouteProtocolVersion = pairingParams.protocolVersion;
|
||||
const insets = useSafeAreaInsets();
|
||||
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
||||
const initialized = useDesktopRemoteStore((s) => s.initialized);
|
||||
@@ -253,7 +266,6 @@ export default function DesktopRemoteScreen() {
|
||||
const [pinInput, setPinInput] = useState('');
|
||||
const [pinClock, setPinClock] = useState(() => Date.now());
|
||||
const [manualBaseUrl, setManualBaseUrl] = useState('');
|
||||
const [manualTicket, setManualTicket] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
void init();
|
||||
@@ -267,11 +279,35 @@ export default function DesktopRemoteScreen() {
|
||||
}, [startDiscovery, stopDiscovery]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof pair === 'string' && pair.trim()) {
|
||||
void pairFromInput(pair);
|
||||
if (typeof pairingRoutePair === 'string' && pairingRoutePair.trim()) {
|
||||
void pairFromInput(pairingRoutePair);
|
||||
router.setParams({ pair: undefined });
|
||||
return;
|
||||
}
|
||||
}, [pair, pairFromInput, router]);
|
||||
if (
|
||||
typeof pairingRouteBaseUrl === 'string' && typeof pairingRouteTicket === 'string' &&
|
||||
typeof pairingRouteEndpointUuid === 'string' && typeof pairingRouteFingerprint === 'string' &&
|
||||
pairingRouteProtocolVersion === '3'
|
||||
) {
|
||||
const url = new URL('astra://desktop-remote');
|
||||
url.searchParams.set('baseUrl', pairingRouteBaseUrl);
|
||||
url.searchParams.set('ticket', pairingRouteTicket);
|
||||
url.searchParams.set('endpointUuid', pairingRouteEndpointUuid);
|
||||
url.searchParams.set('fingerprint', pairingRouteFingerprint);
|
||||
url.searchParams.set('protocolVersion', '3');
|
||||
void pairFromInput(url.toString());
|
||||
router.setParams({ baseUrl: undefined, ticket: undefined, endpointUuid: undefined, fingerprint: undefined, protocolVersion: undefined });
|
||||
}
|
||||
}, [
|
||||
pairFromInput,
|
||||
pairingRouteBaseUrl,
|
||||
pairingRouteEndpointUuid,
|
||||
pairingRouteFingerprint,
|
||||
pairingRoutePair,
|
||||
pairingRouteProtocolVersion,
|
||||
pairingRouteTicket,
|
||||
router,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pinPairing) return undefined;
|
||||
@@ -453,40 +489,31 @@ export default function DesktopRemoteScreen() {
|
||||
<Text variant="body">Manual fallback</Text>
|
||||
</View>
|
||||
<Text variant="caption" color={colors.textSecondary} style={styles.cardCopy}>
|
||||
Enter the desktop URL and pairing code from Astra Desktop.
|
||||
Enter the Astra Desktop HTTPS URL, then compare the six-digit code shown on both devices.
|
||||
</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={manualBaseUrl}
|
||||
onChangeText={setManualBaseUrl}
|
||||
placeholder="http://desktop-ip:38402"
|
||||
placeholder="https://desktop-ip:38402"
|
||||
placeholderTextColor={colors.textTertiary}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
keyboardType="url"
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={manualTicket}
|
||||
onChangeText={setManualTicket}
|
||||
placeholder="Pairing code"
|
||||
placeholderTextColor={colors.textTertiary}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
<Pressable android_ripple={ripple.bounded}
|
||||
style={[
|
||||
styles.secondaryButton,
|
||||
(!manualBaseUrl.trim() || !manualTicket.trim()) && styles.buttonDisabled,
|
||||
!manualBaseUrl.trim() && styles.buttonDisabled,
|
||||
]}
|
||||
disabled={!manualBaseUrl.trim() || !manualTicket.trim()}
|
||||
onPress={() => void pairManual(manualBaseUrl, manualTicket)}
|
||||
disabled={!manualBaseUrl.trim()}
|
||||
onPress={() => void pairManual(manualBaseUrl)}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
color={manualBaseUrl.trim() && manualTicket.trim() ? colors.textPrimary : colors.textTertiary}
|
||||
color={manualBaseUrl.trim() ? colors.textPrimary : colors.textTertiary}
|
||||
>
|
||||
Pair manually
|
||||
Request secure PIN
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
@@ -140,9 +140,9 @@ export default function DesktopSyncScreen() {
|
||||
const lastSummary = useDesktopSyncStore((s) => s.lastSummary);
|
||||
const conflicts = useDesktopSyncStore((s) => s.conflicts);
|
||||
const errorMessage = useDesktopSyncStore((s) => s.errorMessage);
|
||||
const autoSyncEnabled = useDesktopSyncStore((s) => s.autoSyncEnabled);
|
||||
const desktopSyncEnabled = useDesktopSyncStore((s) => s.desktopSyncEnabled);
|
||||
const syncNow = useDesktopSyncStore((s) => s.syncNow);
|
||||
const setAutoSyncEnabled = useDesktopSyncStore((s) => s.setAutoSyncEnabled);
|
||||
const setDesktopSyncEnabled = useDesktopSyncStore((s) => s.setDesktopSyncEnabled);
|
||||
const resolveConflict = useDesktopSyncStore((s) => s.resolveConflict);
|
||||
|
||||
const [connection, setConnection] = useState<DesktopRemoteConnection | null>(null);
|
||||
@@ -229,8 +229,8 @@ export default function DesktopSyncScreen() {
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable android_ripple={ripple.bounded}
|
||||
style={[styles.primaryButton, syncing && styles.disabled]}
|
||||
disabled={syncing}
|
||||
style={[styles.primaryButton, (syncing || !desktopSyncEnabled) && styles.disabled]}
|
||||
disabled={syncing || !desktopSyncEnabled}
|
||||
onPress={() => void syncNow()}
|
||||
accessibilityLabel="Sync favorites and playlists now"
|
||||
>
|
||||
@@ -259,15 +259,15 @@ export default function DesktopSyncScreen() {
|
||||
<View style={styles.card}>
|
||||
<View style={styles.toggleRow}>
|
||||
<View style={styles.toggleText}>
|
||||
<Text variant="body">Sync automatically</Text>
|
||||
<Text variant="body">Desktop Sync</Text>
|
||||
<Text variant="caption" color={colors.textSecondary} style={styles.cardCopy}>
|
||||
Sync when this desktop appears on the network or the app returns to the
|
||||
foreground. Manual and desktop-requested syncs always work.
|
||||
Allow favorites and playlists to sync securely with this desktop on any
|
||||
network. Turn this off to suppress every sync trigger without forgetting it.
|
||||
</Text>
|
||||
</View>
|
||||
<HapticSwitch
|
||||
value={autoSyncEnabled}
|
||||
onValueChange={(value) => void setAutoSyncEnabled(value)}
|
||||
value={desktopSyncEnabled}
|
||||
onValueChange={(value) => void setDesktopSyncEnabled(value)}
|
||||
trackColor={{ false: colors.glassBorder, true: colors.accent }}
|
||||
thumbColor={colors.textPrimary}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user