mirror of
https://github.com/EthanShoeDev/fressh.git
synced 2026-01-11 14:22:51 +00:00
10 KiB
10 KiB
Import Private Key — Detailed Plan
This plan covers implementing private key import (paste or file), improving key management UX, adding metadata display, and enabling copying the corresponding public key. It is grounded in the current code found in:
apps/mobile/src/app/index.tsx(connection form +KeyIdPicker)apps/mobile/src/components/key-manager-modal.tsx(list, generate, rename, delete, set default)apps/mobile/src/lib/secrets-manager.ts(chunked secure store, key metadata, queries)
Relevant Expo docs (for implementation reference):
- Document Picker: https://docs.expo.dev/versions/latest/sdk/document-picker/
- File System: https://docs.expo.dev/versions/latest/sdk/filesystem/
- Clipboard (for copy public key): https://docs.expo.dev/versions/latest/sdk/clipboard/
Current State (from repo)
- Keys are stored via a chunked manifest around
expo-secure-storewith metadata schema:{ priority: number; createdAtMs: int; label?: string; isDefault?: boolean }. KeyManagerModalsupports: generate key (RSA 4096), rename, delete, set default, and selection throughonSelect(already wired and used by the form’sKeyIdPicker).generateKeyPairuses@dylankenneally/react-native-ssh-sftp. We currently save onlypair.privateKey, discard public key.- Selection UI shows label or id; there’s no import flow yet; no createdAt display; no copy public key.
Goals
- Import private keys via two paths:
- Paste PEM/OpenSSH text into a multiline field.
- Pick a key file via a system file picker and read its contents.
- Let users name the key before creating/storing it (applies to both Generate and Import flows).
- Allow users to copy the corresponding public key for any stored private key.
- Show basic metadata (createdAt) in the key list for context.
- Keep storage compatible and robust (no breaking changes to existing manifests).
UX Plan
- Entry points in Key Manager
- Add a secondary action “Import Key” next to the existing “Generate” button.
- When no keys exist, show both “Generate New Key” and “Import Key” prominently in the empty state.
- Import flow (single modal sheet with tabs or steps)
- Step A: Choose method: “Paste PEM” or “Pick File”.
- Paste PEM: multiline
TextInputfor the private key content; live validation feedback. - Pick File: button to open file picker (DocumentPicker). After selection, read file with FileSystem and populate a read-only preview (collapsible) + validation feedback.
- Paste PEM: multiline
- Step B: Name & options
- “Label” text input (required or at least prompted before confirm).
- Optional: “Set as default key” toggle.
- Optional: public key association (see Public Key section below).
- Step C: Confirm & Save
- On success, call
secretsManager.keys.utils.upsertPrivateKeywith computedkeyId, metadata, and value. - Close the import sheet and refresh list.
- On success, call
- Generate flow improvements
- Instead of immediate generate, open a small pre-flight form:
- Label (text input; default prefilled like “My RSA Key”).
- Key type (dropdown/segmented, default RSA).
- Key size for RSA (e.g., 2048/4096; default 4096).
- Optional comment (passed to generator if supported).
- On submit, call
generateKeyPair, thenupsertPrivateKeywith label and useful metadata (see Data Model Changes).
- Key list rows (metadata + actions)
- Under the title, show small muted details: “ID: … • Created: …” (formatted via date-fns).
- Add an overflow/action area with:
- Copy Public Key (uses
expo-clipboard). - Rename (existing).
- Set Default (existing).
- Delete (existing).
- Copy Public Key (uses
- If public key is not known, show “Add Public Key” which opens a small paste dialog to attach one (stored in metadata).
File Picker + File Reading (Expo)
Document selection:
- Use
expo-document-picker:const res = await DocumentPicker.getDocumentAsync({ type: ['text/*', 'application/x-pem-file', 'application/octet-stream'], copyToCacheDirectory: true });- Handle cancelation:
res.canceledor checkres.assets?.lengthdepending on SDK version. - Use
res.assets[0].uriandres.assets[0].name.
File reading:
- Use
expo-file-system:const contents = await FileSystem.readAsStringAsync(uri, { encoding: FileSystem.EncodingType.UTF8 });- If using DocumentPicker with
copyToCacheDirectory: true, the returneduriis readable by FileSystem on both iOS and Android. - Normalize line endings (
\r\n→\n) and trim extraneous whitespace.
Validation basics:
- Accept common formats:
- OpenSSH:
-----BEGIN OPENSSH PRIVATE KEY----- ... -----END OPENSSH PRIVATE KEY----- - RSA:
-----BEGIN RSA PRIVATE KEY----- ... -----END RSA PRIVATE KEY----- - ECDSA/ED25519/ED448/DSA similarly.
- OpenSSH:
- Quick checks:
- Contains a matching BEGIN/END block.
- Reasonable length (e.g., 500–5000 chars); reject empty/suspiciously short.
- Optional: basic type inference from header for metadata.
- Store as-is on success; more advanced parsing is a future enhancement.
Public Key Handling
- Generated keys:
generateKeyPairlikely returns apublicKeyalongsideprivateKey. Store that public key in metadata so users can copy it later. - Imported keys: deriving public keys on-device for all types may require an
extra JS crypto library. To keep scope small and reliable:
- MVP: Allow the user to optionally paste a corresponding public key during import (or later via “Add Public Key”).
- Stretch: Add an optional dependency to derive a public key from the private key (e.g., a pure JS RSA/ed25519 parser) and auto-populate the metadata when feasible.
- UI: “Copy Public Key” button copies
metadata.publicKeyto clipboard; if absent, offer “Add Public Key”.
Data Model Changes (backward-compatible)
- Extend
keyMetadataSchemawith optional fields:publicKey?: string— OpenSSH public key (single line) or PEM.keyType?: SshPrivateKeyType— derived from header or generator input.keySize?: number— for RSA/ECDSA when known.fingerprint?: string— short identifier derived from the private key (see below).- Keep existing:
priority,createdAtMs,label?,isDefault?.
- Compute
keyIdat import/generate time:- Prefer deterministic id using a short fingerprint:
key_<8-char-fp>. - Use
expo-crypto.digestStringAsync('SHA-256', privateKey)→ base16; take first 8–10 chars for id and show full fingerprint in row subtitle if needed. - Fallback to current
key_<timestamp>if hashing ever fails.
- Prefer deterministic id using a short fingerprint:
- Size considerations:
- Metadata is bounded by the ~1KB per-manifest-chunk budget; an OpenSSH public
key line is typically < 1KB, so storing
publicKeyis safe.
- Metadata is bounded by the ~1KB per-manifest-chunk budget; an OpenSSH public
key line is typically < 1KB, so storing
Implementation Steps
- Secrets manager updates
- Update
keyMetadataSchema(optional fields listed above) inapps/mobile/src/lib/secrets-manager.ts. - When generating a key, store
metadata.publicKey,metadata.keyType,metadata.keySize, and computedfingerprint. - Add a tiny helper:
computeKeyFingerprint(privateKey: string): Promise<string>usingexpo-crypto.
- Key Manager modal — UI and flows
- Add “Import Key” entry. Implement a small state machine:
mode = 'list' | 'import' | 'generate'. - Import mode:
- Tabs or toggle between “Paste” and “Pick File”.
- Paste: multiline
TextInput, Validate, Label input, Default toggle, Save. - Pick File: Launch DocumentPicker, read via FileSystem, fill preview, Validate, Label input, Default toggle, Save.
- Generate mode: show label/type/size/comment inputs; on submit generate and upsert.
- List mode rows:
- Show label, id, createdAt (formatted), and default badge.
- Row actions: Select (radio), Copy Public Key, Rename, Set Default, Delete.
- If no
metadata.publicKey, action shows “Add Public Key” and opens a small paste dialog; on save, upsert metadata only.
- Copy public key
- Use
expo-clipboard:await Clipboard.setStringAsync(metadata.publicKey)- Show a small toast/snackbar or inline confirmation.
- Date formatting
- Add
date-fnsand format with eitherformat(e.g.,MMM d, yyyy) orformatDistanceToNowfor a relative display. Placement: muted line under the label in each row.
- Validation utilities
- Add a small util module:
parseSshPrivateKey.tswith functions:detectPrivateKeyType(pem: string): SshPrivateKeyType | 'openssh' | undefinedisLikelyValidPrivateKey(pem: string): boolean- These are string/regex based for MVP; we can replace with stronger parsing later.
- Error handling
- Show inline errors in the import modal (invalid format, unreadable file, empty input).
- Disable Save until validation passes.
Dependencies to add
expo-document-picker— file selection.expo-file-system— read selected file contents.expo-clipboard— copy public key.date-fns— format timestamps.
Notes:
- Ensure proper installation via
npx expo install expo-document-picker expo-file-system expo-clipboard date-fns. - For DocumentPicker, prefer
copyToCacheDirectory: trueto guarantee a readableuri.
Security & Privacy
- Never log key material or show it in error messages.
- Keep logs behind a debug flag; redact key strings in any analytics.
- Continue storing private keys in
SecureStore(already handled), avoid moving them to less secure storage.
Testing Plan
- Unit tests for the validation utils with sample PEM strings.
- Manual QA on devices/simulators:
- Paste invalid/valid keys (OpenSSH, RSA) → proper errors/success.
- Pick a
.pem/no-extension text file → reads, validates, stores. - Rename, set default, delete flows unchanged.
- Copy public key present vs. absent → “Add Public Key” flow wiring.
- CreatedAt formatting appears correctly and is stable across renames.
Implementation Notes (cross-cutting)
- Keep all key writes going through
upsertPrivateKeyso React Query invalidation stays consistent. - Preserve
createdAtMson upserts (already implemented), addfingerprintonly once when first inserted. - If adding new metadata fields, mark them optional in zod to avoid breaking existing entries.