mirror of
https://github.com/EthanShoeDev/fressh.git
synced 2026-01-11 14:22:51 +00:00
working private key
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
|
import * as Crypto from 'expo-crypto';
|
||||||
|
import * as DocumentPicker from 'expo-document-picker';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Pressable, ScrollView, Text, TextInput, View } from 'react-native';
|
import { Pressable, ScrollView, Text, TextInput, View } from 'react-native';
|
||||||
import { secretsManager } from '@/lib/secrets-manager';
|
import { secretsManager } from '@/lib/secrets-manager';
|
||||||
|
import { useTheme } from '@/lib/theme';
|
||||||
|
|
||||||
export type KeyListMode = 'manage' | 'select';
|
export type KeyListMode = 'manage' | 'select';
|
||||||
|
|
||||||
@@ -10,6 +13,7 @@ export function KeyList(props: {
|
|||||||
onSelect?: (id: string) => void | Promise<void>;
|
onSelect?: (id: string) => void | Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const listKeysQuery = useQuery(secretsManager.keys.query.list);
|
const listKeysQuery = useQuery(secretsManager.keys.query.list);
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
const generateMutation = useMutation({
|
const generateMutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
@@ -28,15 +32,16 @@ export function KeyList(props: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView contentContainerStyle={{ padding: 16 }}>
|
<ScrollView contentContainerStyle={{ padding: 16, gap: 16 }}>
|
||||||
|
<ImportKeyCard onImported={() => listKeysQuery.refetch()} />
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
backgroundColor: '#2563EB',
|
backgroundColor: theme.colors.primary,
|
||||||
borderRadius: 10,
|
borderRadius: 12,
|
||||||
paddingVertical: 12,
|
paddingVertical: 14,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 12,
|
|
||||||
},
|
},
|
||||||
generateMutation.isPending && { opacity: 0.7 },
|
generateMutation.isPending && { opacity: 0.7 },
|
||||||
]}
|
]}
|
||||||
@@ -45,19 +50,26 @@ export function KeyList(props: {
|
|||||||
generateMutation.mutate();
|
generateMutation.mutate();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ color: '#FFFFFF', fontWeight: '700', fontSize: 14 }}>
|
<Text
|
||||||
|
style={{
|
||||||
|
color: theme.colors.buttonTextOnPrimary,
|
||||||
|
fontWeight: '700',
|
||||||
|
fontSize: 14,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{generateMutation.isPending
|
{generateMutation.isPending
|
||||||
? 'Generating…'
|
? 'Generating…'
|
||||||
: 'Generate New RSA 4096 Key'}
|
: 'Generate New Key (ed25519)'}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
{listKeysQuery.isLoading ? (
|
{listKeysQuery.isLoading ? (
|
||||||
<Text style={{ color: '#9AA0A6' }}>Loading keys…</Text>
|
<Text style={{ color: theme.colors.muted }}>Loading keys…</Text>
|
||||||
) : listKeysQuery.isError ? (
|
) : listKeysQuery.isError ? (
|
||||||
<Text style={{ color: '#FCA5A5' }}>Error loading keys</Text>
|
<Text style={{ color: theme.colors.danger }}>Error loading keys</Text>
|
||||||
) : listKeysQuery.data?.length ? (
|
) : listKeysQuery.data?.length ? (
|
||||||
<View>
|
<View style={{ gap: 12 }}>
|
||||||
{listKeysQuery.data.map((k) => (
|
{listKeysQuery.data.map((k) => (
|
||||||
<KeyRow
|
<KeyRow
|
||||||
key={k.id}
|
key={k.id}
|
||||||
@@ -68,17 +80,261 @@ export function KeyList(props: {
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<Text style={{ color: '#9AA0A6' }}>No keys yet</Text>
|
<Text style={{ color: theme.colors.muted }}>No keys yet</Text>
|
||||||
)}
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ImportKeyCard({ onImported }: { onImported: () => void }) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [mode, setMode] = React.useState<'paste' | 'file'>('paste');
|
||||||
|
const [label, setLabel] = React.useState('Imported Key');
|
||||||
|
const [asDefault, setAsDefault] = React.useState(false);
|
||||||
|
const [content, setContent] = React.useState('');
|
||||||
|
const [fileName, setFileName] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
const importMutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const trimmed = content.trim();
|
||||||
|
if (!trimmed) throw new Error('No key content provided');
|
||||||
|
const keyId = `key_${Crypto.randomUUID()}`;
|
||||||
|
await secretsManager.keys.utils.upsertPrivateKey({
|
||||||
|
keyId,
|
||||||
|
metadata: {
|
||||||
|
priority: 0,
|
||||||
|
label: label || 'Imported Key',
|
||||||
|
isDefault: asDefault,
|
||||||
|
},
|
||||||
|
value: trimmed,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
setContent('');
|
||||||
|
setFileName(null);
|
||||||
|
onImported();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pickFile = React.useCallback(async () => {
|
||||||
|
const res = await DocumentPicker.getDocumentAsync({
|
||||||
|
multiple: false,
|
||||||
|
copyToCacheDirectory: true,
|
||||||
|
type: ['text/*', 'application/*'],
|
||||||
|
});
|
||||||
|
// Newer expo-document-picker: { canceled: boolean, assets?: [{ uri, name, ... }] }
|
||||||
|
const canceled = 'canceled' in res ? res.canceled : false;
|
||||||
|
if (canceled) return;
|
||||||
|
const asset = res.assets?.[0];
|
||||||
|
const file = asset?.file;
|
||||||
|
if (!file) return;
|
||||||
|
setFileName(asset.name ?? null);
|
||||||
|
const data = await file.text();
|
||||||
|
setContent(data);
|
||||||
|
if (asset.name && (!label || label === 'Imported Key')) {
|
||||||
|
setLabel(String(asset.name).replace(/\.[^.]+$/, ''));
|
||||||
|
}
|
||||||
|
}, [label]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: theme.colors.surface,
|
||||||
|
borderRadius: 12,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
padding: 12,
|
||||||
|
gap: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textPrimary,
|
||||||
|
fontWeight: '700',
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Import Private Key
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
backgroundColor: theme.colors.inputBackground,
|
||||||
|
borderRadius: 10,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(['paste', 'file'] as const).map((m) => (
|
||||||
|
<Pressable
|
||||||
|
key={m}
|
||||||
|
onPress={() => setMode(m)}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
paddingVertical: 10,
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor:
|
||||||
|
mode === m
|
||||||
|
? theme.colors.surface
|
||||||
|
: theme.colors.inputBackground,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
mode === m ? theme.colors.textPrimary : theme.colors.muted,
|
||||||
|
fontWeight: '600',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{m === 'paste' ? 'Paste' : 'File'}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{mode === 'paste' ? (
|
||||||
|
<TextInput
|
||||||
|
multiline
|
||||||
|
placeholder="Paste your private key here"
|
||||||
|
placeholderTextColor={theme.colors.muted}
|
||||||
|
value={content}
|
||||||
|
onChangeText={setContent}
|
||||||
|
style={{
|
||||||
|
minHeight: 120,
|
||||||
|
backgroundColor: theme.colors.inputBackground,
|
||||||
|
color: theme.colors.textPrimary,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: 12,
|
||||||
|
fontFamily: 'Menlo, ui-monospace, monospace',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<View style={{ gap: 8 }}>
|
||||||
|
<Pressable
|
||||||
|
onPress={pickFile}
|
||||||
|
style={{
|
||||||
|
backgroundColor: theme.colors.transparent,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
borderRadius: 10,
|
||||||
|
paddingVertical: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{ color: theme.colors.textSecondary, fontWeight: '600' }}
|
||||||
|
>
|
||||||
|
{fileName ? 'Choose Different File' : 'Choose File'}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
{fileName ? (
|
||||||
|
<Text style={{ color: theme.colors.muted }}>
|
||||||
|
Selected: {fileName}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
{content ? (
|
||||||
|
<TextInput
|
||||||
|
editable={false}
|
||||||
|
multiline
|
||||||
|
value={content.slice(0, 500)}
|
||||||
|
style={{
|
||||||
|
minHeight: 80,
|
||||||
|
backgroundColor: theme.colors.inputBackground,
|
||||||
|
color: theme.colors.textSecondary,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: 10,
|
||||||
|
fontFamily: 'Menlo, ui-monospace, monospace',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={{ gap: 8 }}>
|
||||||
|
<Text style={{ color: theme.colors.textSecondary, fontSize: 12 }}>
|
||||||
|
Label
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Display name"
|
||||||
|
placeholderTextColor={theme.colors.muted}
|
||||||
|
value={label}
|
||||||
|
onChangeText={setLabel}
|
||||||
|
style={{
|
||||||
|
backgroundColor: theme.colors.inputBackground,
|
||||||
|
color: theme.colors.textPrimary,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
borderRadius: 10,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 10,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setAsDefault((v) => !v)}
|
||||||
|
style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
borderRadius: 6,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
backgroundColor: asDefault
|
||||||
|
? theme.colors.primary
|
||||||
|
: theme.colors.transparent,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text style={{ color: theme.colors.textSecondary }}>
|
||||||
|
Set as default
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
|
<Pressable
|
||||||
|
disabled={importMutation.isPending}
|
||||||
|
onPress={() => importMutation.mutate()}
|
||||||
|
style={{
|
||||||
|
backgroundColor: theme.colors.primary,
|
||||||
|
borderRadius: 12,
|
||||||
|
paddingVertical: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
opacity: importMutation.isPending ? 0.6 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: theme.colors.buttonTextOnPrimary,
|
||||||
|
fontWeight: '700',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{importMutation.isPending ? 'Importing…' : 'Import Key'}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
|
{importMutation.isError ? (
|
||||||
|
<Text style={{ color: theme.colors.danger }}>
|
||||||
|
{(importMutation.error as Error).message || 'Import failed'}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function KeyRow(props: {
|
function KeyRow(props: {
|
||||||
entryId: string;
|
entryId: string;
|
||||||
mode: KeyListMode;
|
mode: KeyListMode;
|
||||||
onSelected?: (id: string) => void | Promise<void>;
|
onSelected?: (id: string) => void | Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
|
const theme = useTheme();
|
||||||
const entryQuery = useQuery(secretsManager.keys.query.get(props.entryId));
|
const entryQuery = useQuery(secretsManager.keys.query.get(props.entryId));
|
||||||
const entry = entryQuery.data;
|
const entry = entryQuery.data;
|
||||||
const [label, setLabel] = React.useState(
|
const [label, setLabel] = React.useState(
|
||||||
@@ -141,30 +397,35 @@ function KeyRow(props: {
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
backgroundColor: '#0E172B',
|
backgroundColor: theme.colors.inputBackground,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#2A3655',
|
borderColor: theme.colors.border,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
marginBottom: 10,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={{ flex: 1, marginRight: 8 }}>
|
<View style={{ flex: 1, marginRight: 8 }}>
|
||||||
<Text style={{ color: '#E5E7EB', fontSize: 15, fontWeight: '600' }}>
|
<Text
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textPrimary,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: '600',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{entry.manifestEntry.metadata.label ?? entry.manifestEntry.id}
|
{entry.manifestEntry.metadata.label ?? entry.manifestEntry.id}
|
||||||
{entry.manifestEntry.metadata.isDefault ? ' • Default' : ''}
|
{entry.manifestEntry.metadata.isDefault ? ' • Default' : ''}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ color: '#9AA0A6', fontSize: 12, marginTop: 2 }}>
|
<Text style={{ color: theme.colors.muted, fontSize: 12, marginTop: 2 }}>
|
||||||
ID: {entry.manifestEntry.id}
|
ID: {entry.manifestEntry.id}
|
||||||
</Text>
|
</Text>
|
||||||
{props.mode === 'manage' ? (
|
{props.mode === 'manage' ? (
|
||||||
<TextInput
|
<TextInput
|
||||||
style={{
|
style={{
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#2A3655',
|
borderColor: theme.colors.border,
|
||||||
backgroundColor: '#0E172B',
|
backgroundColor: theme.colors.inputBackground,
|
||||||
color: '#E5E7EB',
|
color: theme.colors.textPrimary,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
@@ -172,7 +433,7 @@ function KeyRow(props: {
|
|||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
}}
|
}}
|
||||||
placeholder="Display name"
|
placeholder="Display name"
|
||||||
placeholderTextColor="#9AA0A6"
|
placeholderTextColor={theme.colors.muted}
|
||||||
value={label}
|
value={label}
|
||||||
onChangeText={setLabel}
|
onChangeText={setLabel}
|
||||||
/>
|
/>
|
||||||
@@ -185,14 +446,20 @@ function KeyRow(props: {
|
|||||||
setDefaultMutation.mutate();
|
setDefaultMutation.mutate();
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#2563EB',
|
backgroundColor: theme.colors.primary,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ color: '#FFFFFF', fontWeight: '700', fontSize: 12 }}>
|
<Text
|
||||||
|
style={{
|
||||||
|
color: theme.colors.buttonTextOnPrimary,
|
||||||
|
fontWeight: '700',
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
Select
|
Select
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@@ -201,9 +468,9 @@ function KeyRow(props: {
|
|||||||
<Pressable
|
<Pressable
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: theme.colors.transparent,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#2A3655',
|
borderColor: theme.colors.border,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
@@ -216,7 +483,13 @@ function KeyRow(props: {
|
|||||||
}}
|
}}
|
||||||
disabled={renameMutation.isPending}
|
disabled={renameMutation.isPending}
|
||||||
>
|
>
|
||||||
<Text style={{ color: '#C6CBD3', fontWeight: '600', fontSize: 12 }}>
|
<Text
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textSecondary,
|
||||||
|
fontWeight: '600',
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{renameMutation.isPending ? 'Saving…' : 'Save'}
|
{renameMutation.isPending ? 'Saving…' : 'Save'}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@@ -224,9 +497,9 @@ function KeyRow(props: {
|
|||||||
{!entry.manifestEntry.metadata.isDefault ? (
|
{!entry.manifestEntry.metadata.isDefault ? (
|
||||||
<Pressable
|
<Pressable
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: theme.colors.transparent,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#2A3655',
|
borderColor: theme.colors.border,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
@@ -236,16 +509,22 @@ function KeyRow(props: {
|
|||||||
setDefaultMutation.mutate();
|
setDefaultMutation.mutate();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ color: '#C6CBD3', fontWeight: '600', fontSize: 12 }}>
|
<Text
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textSecondary,
|
||||||
|
fontWeight: '600',
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
Set Default
|
Set Default
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
) : null}
|
) : null}
|
||||||
<Pressable
|
<Pressable
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: theme.colors.transparent,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#7F1D1D',
|
borderColor: theme.colors.danger,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
@@ -255,7 +534,13 @@ function KeyRow(props: {
|
|||||||
deleteMutation.mutate();
|
deleteMutation.mutate();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ color: '#FCA5A5', fontWeight: '700', fontSize: 12 }}>
|
<Text
|
||||||
|
style={{
|
||||||
|
color: theme.colors.danger,
|
||||||
|
fontWeight: '700',
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
Delete
|
Delete
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|||||||
@@ -18,17 +18,25 @@ export const useSshConnMutation = () => {
|
|||||||
mutationFn: async (connectionDetails: InputConnectionDetails) => {
|
mutationFn: async (connectionDetails: InputConnectionDetails) => {
|
||||||
try {
|
try {
|
||||||
console.log('Connecting to SSH server...');
|
console.log('Connecting to SSH server...');
|
||||||
|
// Resolve security into the RN bridge shape
|
||||||
|
const security =
|
||||||
|
connectionDetails.security.type === 'password'
|
||||||
|
? {
|
||||||
|
type: 'password' as const,
|
||||||
|
password: connectionDetails.security.password,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
type: 'key' as const,
|
||||||
|
privateKey: await secretsManager.keys.utils
|
||||||
|
.getPrivateKey(connectionDetails.security.keyId)
|
||||||
|
.then((e) => e.value),
|
||||||
|
};
|
||||||
|
|
||||||
const sshConnection = await RnRussh.connect({
|
const sshConnection = await RnRussh.connect({
|
||||||
host: connectionDetails.host,
|
host: connectionDetails.host,
|
||||||
port: connectionDetails.port,
|
port: connectionDetails.port,
|
||||||
username: connectionDetails.username,
|
username: connectionDetails.username,
|
||||||
security:
|
security,
|
||||||
connectionDetails.security.type === 'password'
|
|
||||||
? {
|
|
||||||
type: 'password',
|
|
||||||
password: connectionDetails.security.password,
|
|
||||||
}
|
|
||||||
: { type: 'key', privateKey: 'TODO' },
|
|
||||||
onStatusChange: (status) => {
|
onStatusChange: (status) => {
|
||||||
console.log('SSH connection status', status);
|
console.log('SSH connection status', status);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ use tokio::sync::{broadcast, Mutex as AsyncMutex};
|
|||||||
|
|
||||||
use russh::{self, client, ChannelMsg, Disconnect};
|
use russh::{self, client, ChannelMsg, Disconnect};
|
||||||
use russh::client::{Config as ClientConfig, Handle as ClientHandle};
|
use russh::client::{Config as ClientConfig, Handle as ClientHandle};
|
||||||
use russh_keys::{Algorithm as KeyAlgorithm, EcdsaCurve, PrivateKey};
|
use russh_keys::{Algorithm as KeyAlgorithm, EcdsaCurve, PrivateKey as RusshKeysPrivateKey};
|
||||||
|
use russh::keys::{PrivateKey as RusshPrivateKey, PrivateKeyWithHashAlg};
|
||||||
use russh_keys::ssh_key::{self, LineEnding};
|
use russh_keys::ssh_key::{self, LineEnding};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
|
||||||
@@ -698,10 +699,20 @@ pub async fn connect(options: ConnectOptions) -> Result<Arc<SSHConnection>, SshE
|
|||||||
// Auth
|
// Auth
|
||||||
let auth = match &details.security {
|
let auth = match &details.security {
|
||||||
Security::Password { password } => {
|
Security::Password { password } => {
|
||||||
handle.authenticate_password(details.username.clone(), password.clone()).await?
|
handle
|
||||||
|
.authenticate_password(details.username.clone(), password.clone())
|
||||||
|
.await?
|
||||||
}
|
}
|
||||||
Security::Key { .. } => {
|
// Treat key_id as the OpenSSH PEM-encoded private key content
|
||||||
return Err(SshError::UnsupportedKeyType);
|
Security::Key { key_id } => {
|
||||||
|
// Parse OpenSSH private key text into a russh::keys::PrivateKey
|
||||||
|
let parsed: RusshPrivateKey = RusshPrivateKey::from_openssh(key_id.as_str())
|
||||||
|
.map_err(|e| SshError::RusshKeys(e.to_string()))?;
|
||||||
|
// Wrap; omit hash preference (server selects or default applies)
|
||||||
|
let pk_with_hash = PrivateKeyWithHashAlg::new(Arc::new(parsed), None);
|
||||||
|
handle
|
||||||
|
.authenticate_publickey(details.username.clone(), pk_with_hash)
|
||||||
|
.await?
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match auth {
|
match auth {
|
||||||
@@ -729,12 +740,12 @@ pub async fn connect(options: ConnectOptions) -> Result<Arc<SSHConnection>, SshE
|
|||||||
pub async fn generate_key_pair(key_type: KeyType) -> Result<String, SshError> {
|
pub async fn generate_key_pair(key_type: KeyType) -> Result<String, SshError> {
|
||||||
let mut rng = OsRng;
|
let mut rng = OsRng;
|
||||||
let key = match key_type {
|
let key = match key_type {
|
||||||
KeyType::Rsa => PrivateKey::random(&mut rng, KeyAlgorithm::Rsa { hash: None })?,
|
KeyType::Rsa => RusshKeysPrivateKey::random(&mut rng, KeyAlgorithm::Rsa { hash: None })?,
|
||||||
KeyType::Ecdsa => PrivateKey::random(
|
KeyType::Ecdsa => RusshKeysPrivateKey::random(
|
||||||
&mut rng,
|
&mut rng,
|
||||||
KeyAlgorithm::Ecdsa { curve: EcdsaCurve::NistP256 },
|
KeyAlgorithm::Ecdsa { curve: EcdsaCurve::NistP256 },
|
||||||
)?,
|
)?,
|
||||||
KeyType::Ed25519 => PrivateKey::random(&mut rng, KeyAlgorithm::Ed25519)?,
|
KeyType::Ed25519 => RusshKeysPrivateKey::random(&mut rng, KeyAlgorithm::Ed25519)?,
|
||||||
KeyType::Ed448 => return Err(SshError::UnsupportedKeyType),
|
KeyType::Ed448 => return Err(SshError::UnsupportedKeyType),
|
||||||
};
|
};
|
||||||
let pem = key.to_openssh(LineEnding::LF)?; // Zeroizing<String>
|
let pem = key.to_openssh(LineEnding::LF)?; // Zeroizing<String>
|
||||||
|
|||||||
Reference in New Issue
Block a user