mirror of
https://github.com/EthanShoeDev/fressh.git
synced 2026-01-11 06:12:51 +00:00
249 lines
10 KiB
Markdown
249 lines
10 KiB
Markdown
# 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-store` with
|
||
metadata schema:
|
||
`{ priority: number; createdAtMs: int; label?: string; isDefault?: boolean }`.
|
||
- `KeyManagerModal` supports: generate key (RSA 4096), rename, delete, set
|
||
default, and selection through `onSelect` (already wired and used by the
|
||
form’s `KeyIdPicker`).
|
||
- `generateKeyPair` uses `@dylankenneally/react-native-ssh-sftp`. We currently
|
||
save only `pair.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
|
||
|
||
1. 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.
|
||
|
||
2. Import flow (single modal sheet with tabs or steps)
|
||
|
||
- Step A: Choose method: “Paste PEM” or “Pick File”.
|
||
- Paste PEM: multiline `TextInput` for 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.
|
||
- 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.upsertPrivateKey` with computed
|
||
`keyId`, metadata, and value.
|
||
- Close the import sheet and refresh list.
|
||
|
||
3. 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`, then `upsertPrivateKey` with label and
|
||
useful metadata (see Data Model Changes).
|
||
|
||
4. 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).
|
||
- 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.canceled` or check `res.assets?.length` depending
|
||
on SDK version.
|
||
- Use `res.assets[0].uri` and `res.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 returned
|
||
`uri` is 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.
|
||
- 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: `generateKeyPair` likely returns a `publicKey` alongside
|
||
`privateKey`. 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.publicKey` to clipboard; if
|
||
absent, offer “Add Public Key”.
|
||
|
||
## Data Model Changes (backward-compatible)
|
||
|
||
- Extend `keyMetadataSchema` with 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 `keyId` at 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.
|
||
- Size considerations:
|
||
- Metadata is bounded by the ~1KB per-manifest-chunk budget; an OpenSSH public
|
||
key line is typically < 1KB, so storing `publicKey` is safe.
|
||
|
||
## Implementation Steps
|
||
|
||
1. Secrets manager updates
|
||
|
||
- Update `keyMetadataSchema` (optional fields listed above) in
|
||
`apps/mobile/src/lib/secrets-manager.ts`.
|
||
- When generating a key, store `metadata.publicKey`, `metadata.keyType`,
|
||
`metadata.keySize`, and computed `fingerprint`.
|
||
- Add a tiny helper:
|
||
`computeKeyFingerprint(privateKey: string): Promise<string>` using
|
||
`expo-crypto`.
|
||
|
||
2. 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.
|
||
|
||
3. Copy public key
|
||
|
||
- Use `expo-clipboard`:
|
||
- `await Clipboard.setStringAsync(metadata.publicKey)`
|
||
- Show a small toast/snackbar or inline confirmation.
|
||
|
||
4. Date formatting
|
||
|
||
- Add `date-fns` and format with either `format` (e.g., `MMM d, yyyy`) or
|
||
`formatDistanceToNow` for a relative display. Placement: muted line under the
|
||
label in each row.
|
||
|
||
5. Validation utilities
|
||
|
||
- Add a small util module: `parseSshPrivateKey.ts` with functions:
|
||
- `detectPrivateKeyType(pem: string): SshPrivateKeyType | 'openssh' | undefined`
|
||
- `isLikelyValidPrivateKey(pem: string): boolean`
|
||
- These are string/regex based for MVP; we can replace with stronger parsing
|
||
later.
|
||
|
||
6. 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: true` to guarantee a
|
||
readable `uri`.
|
||
|
||
## 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 `upsertPrivateKey` so React Query
|
||
invalidation stays consistent.
|
||
- Preserve `createdAtMs` on upserts (already implemented), add `fingerprint`
|
||
only once when first inserted.
|
||
- If adding new metadata fields, mark them optional in zod to avoid breaking
|
||
existing entries.
|