diff --git a/apps/mobile/src/app/index.tsx b/apps/mobile/src/app/index.tsx index d188831..9d91d74 100644 --- a/apps/mobile/src/app/index.tsx +++ b/apps/mobile/src/app/index.tsx @@ -348,23 +348,29 @@ const styles = StyleSheet.create({ }, card: { backgroundColor: '#111B34', - borderRadius: 16, - padding: 20, + borderRadius: 20, + padding: 24, + marginHorizontal: 4, shadowColor: '#000', - shadowOpacity: 0.2, - shadowRadius: 12, - elevation: 6, + shadowOpacity: 0.3, + shadowRadius: 16, + shadowOffset: { width: 0, height: 4 }, + elevation: 8, + borderWidth: 1, + borderColor: '#1E293B', }, title: { - fontSize: 22, + fontSize: 24, fontWeight: '700', color: '#E5E7EB', - marginBottom: 4, + marginBottom: 6, + letterSpacing: 0.5, }, subtitle: { - fontSize: 14, + fontSize: 15, color: '#9AA0A6', - marginBottom: 16, + marginBottom: 24, + lineHeight: 20, }, inputGroup: { marginBottom: 12, @@ -391,7 +397,7 @@ const styles = StyleSheet.create({ fontSize: 12, }, actions: { - marginTop: 8, + marginTop: 20, }, mutedText: { color: '#9AA0A6', @@ -399,14 +405,20 @@ const styles = StyleSheet.create({ }, submitButton: { backgroundColor: '#2563EB', - borderRadius: 10, - paddingVertical: 14, + borderRadius: 12, + paddingVertical: 16, alignItems: 'center', + shadowColor: '#2563EB', + shadowOpacity: 0.3, + shadowRadius: 8, + shadowOffset: { width: 0, height: 2 }, + elevation: 4, }, submitButtonText: { color: '#FFFFFF', fontWeight: '700', fontSize: 16, + letterSpacing: 0.5, }, buttonDisabled: { backgroundColor: '#3B82F6', @@ -416,15 +428,16 @@ const styles = StyleSheet.create({ backgroundColor: 'transparent', borderWidth: 1, borderColor: '#2A3655', - borderRadius: 10, - paddingVertical: 12, + borderRadius: 12, + paddingVertical: 14, alignItems: 'center', - marginTop: 8, + marginTop: 12, }, secondaryButtonText: { color: '#C6CBD3', fontWeight: '600', fontSize: 14, + letterSpacing: 0.3, }, listSection: { marginTop: 20, diff --git a/apps/mobile/src/components/form-components.tsx b/apps/mobile/src/components/form-components.tsx index ab66c32..4e30511 100644 --- a/apps/mobile/src/components/form-components.tsx +++ b/apps/mobile/src/components/form-components.tsx @@ -108,13 +108,16 @@ export function PickerField( return ( {label ? {label} : null} - - selectedValue={field.state.value} - onValueChange={(itemValue) => field.handleChange(itemValue)} - {...rest} - > - {props.children} - + + + style={styles.picker} + selectedValue={field.state.value} + onValueChange={(itemValue) => field.handleChange(itemValue)} + {...rest} + > + {props.children} + + ); @@ -171,7 +174,7 @@ export const { useAppForm, withForm, withFieldGroup } = createFormHook({ const styles = StyleSheet.create({ inputGroup: { - marginBottom: 12, + marginBottom: 16, }, label: { marginBottom: 6, @@ -214,4 +217,11 @@ const styles = StyleSheet.create({ color: '#FCA5A5', fontSize: 12, }, + pickerContainer: { + paddingHorizontal: 8, + paddingVertical: 4, + }, + picker: { + color: '#E5E7EB', + }, }); diff --git a/apps/mobile/src/lib/secrets-manager.ts b/apps/mobile/src/lib/secrets-manager.ts index f3505f6..ee226b5 100644 --- a/apps/mobile/src/lib/secrets-manager.ts +++ b/apps/mobile/src/lib/secrets-manager.ts @@ -7,6 +7,7 @@ import { queryClient } from './utils'; const keys = { storagePrefix: 'privateKey_', manifestKey: 'privateKeysManifest', + chunkSize: 1800, // Safely under 2048 byte limit } as const; const keyManifestSchema = z.object({ @@ -15,7 +16,8 @@ const keyManifestSchema = z.object({ z.object({ id: z.string(), priority: z.number(), - createdAt: z.date(), + createdAtMs: z.int(), + chunkCount: z.number().default(1), }), ), }); @@ -31,6 +33,19 @@ async function getKeyManifest() { return keyManifestSchema.parse(manifest); } +// Utility functions for chunking large data +function splitIntoChunks(data: string, chunkSize: number): string[] { + const chunks: string[] = []; + for (let i = 0; i < data.length; i += chunkSize) { + chunks.push(data.substring(i, i + chunkSize)); + } + return chunks; +} + +function getChunkKey(keyId: string, chunkIndex: number): string { + return `${keys.storagePrefix}${keyId}_chunk_${chunkIndex}`; +} + async function savePrivateKey(params: { keyId: string; privateKey: string; @@ -42,17 +57,24 @@ async function savePrivateKey(params: { if (existingKey) throw new Error('Key already exists'); + // Split the private key into chunks if it's too large + const chunks = splitIntoChunks(params.privateKey, keys.chunkSize); + const chunkCount = chunks.length; + const newKey = { id: params.keyId, priority: params.priority, - createdAt: new Date(), + createdAtMs: Date.now(), + chunkCount, }; manifest.keys.push(newKey); - await SecureStore.setItemAsync( - `${keys.storagePrefix}${params.keyId}`, - params.privateKey, - ); + + // Save each chunk separately + for (let i = 0; i < chunks.length; i++) { + await SecureStore.setItemAsync(getChunkKey(params.keyId, i), chunks[i]!); + } + await SecureStore.setItemAsync(keys.manifestKey, JSON.stringify(manifest)); await queryClient.invalidateQueries({ queryKey: [keyQueryKey] }); } @@ -61,10 +83,16 @@ async function getPrivateKey(keyId: string) { const manifest = await getKeyManifest(); const key = manifest.keys.find((key) => key.id === keyId); if (!key) throw new Error('Key not found'); - const privateKey = await SecureStore.getItemAsync( - `${keys.storagePrefix}${keyId}`, - ); - if (!privateKey) throw new Error('Key not found'); + + // Reassemble the private key from chunks + const chunks: string[] = []; + for (let i = 0; i < key.chunkCount; i++) { + const chunk = await SecureStore.getItemAsync(getChunkKey(keyId, i)); + if (!chunk) throw new Error(`Key chunk ${i} not found`); + chunks.push(chunk); + } + + const privateKey = chunks.join(''); return { ...key, privateKey, @@ -75,9 +103,15 @@ async function deletePrivateKey(keyId: string) { const manifest = await getKeyManifest(); const key = manifest.keys.find((key) => key.id === keyId); if (!key) throw new Error('Key not found'); + manifest.keys = manifest.keys.filter((key) => key.id !== keyId); + + // Delete all chunks for this key + for (let i = 0; i < key.chunkCount; i++) { + await SecureStore.deleteItemAsync(getChunkKey(keyId, i)); + } + await SecureStore.setItemAsync(keys.manifestKey, JSON.stringify(manifest)); - await SecureStore.deleteItemAsync(`${keys.storagePrefix}${keyId}`); await queryClient.invalidateQueries({ queryKey: [keyQueryKey] }); } @@ -235,9 +269,9 @@ async function generateKeyPair(params: { }) { const keyPair = await SSHClient.generateKeyPair( params.type, - params.passphrase, + params.passphrase ?? '', params.keySize, - params.comment, + params.comment ?? '', ); return keyPair; }