pdftract/notes/pdftract-2ka7.md
jedarden 77a8a6d7f3 feat(pdftract-2ka7): implement secure password ingress channels
Implement TH-07 password ingress channels for CLI:
- --password-stdin flag (reads one line from stdin)
- PDFTRACT_PASSWORD env var
- --password VALUE (rejected unless PDFTRACT_INSECURE_CLI_PASSWORD=1)

Exit code 64 for insecure password usage with stderr hint.
Stderr warning emitted when --password VALUE accepted via opt-in.

Priority order: stdin > env var > value (opt-in) > none.
Empty password (bare newline) treated as no password.

Acceptance criteria:
- --password-stdin: PASS
- PDFTRACT_PASSWORD: PASS
- --password VALUE rejection (exit 64): PASS
- Stderr warning on opt-in: PASS
- Exit codes: PASS
- Python/MCP/Serve: N/A (crates don't exist yet)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 02:20:02 -04:00

93 lines
3.6 KiB
Markdown

# pdftract-2ka7: Password Ingress Channels
## Summary
Implemented secure password ingress channels for PDF password handling in the CLI.
## Implementation Status
### CLI (pdftract-cli) ✅ PASS
**File:** `crates/pdftract-cli/src/password.rs`
Implemented the complete password resolution logic with priority order:
1. `--password-stdin` flag (reads one line from stdin)
2. `PDFTRACT_PASSWORD` env var
3. `--password VALUE` (only if `PDFTRACT_INSECURE_CLI_PASSWORD=1`)
4. None
**File:** `crates/pdftract-cli/src/main.rs`
Wired the password resolution into the `Extract` command with proper error handling.
### Acceptance Criteria
| Criterion | Status | Notes |
|-----------|--------|-------|
| `--password-stdin` flag implemented | ✅ PASS | Reads one newline-terminated line from stdin; empty password = no password |
| `PDFTRACT_PASSWORD` env var | ✅ PASS | Read when present and `--password-stdin` not supplied |
| `--password VALUE` rejected (exit 64) | ✅ PASS | Requires `PDFTRACT_INSECURE_CLI_PASSWORD=1` |
| Stderr warning on opt-in | ✅ PASS | Emits warning when `--password VALUE` accepted |
| Python `password=` kwarg | ⚠️ N/A | `pdftract-py` crate doesn't exist yet |
| MCP password body field | ⚠️ N/A | `pdftract-mcp` crate doesn't exist yet |
| Serve password form field | ⚠️ N/A | `pdftract-serve` crate doesn't exist yet |
| Exit codes match policy | ✅ PASS | Exit code 64 for usage errors |
| TH-07 test passes | ⚠️ WARN | Separate bead (not implemented yet) |
## Test Results
### Unit Tests
```
running 8 tests
test password::tests::test_resolve_password_empty_env_var ... ok
test password::tests::test_resolve_password_opt_in_two_not_accepted ... ok
test password::tests::test_resolve_password_value_rejected_without_opt_in ... ok
test password::tests::test_resolve_password_opt_in_zero_not_accepted ... ok
test password::tests::test_resolve_password_stdin_takes_priority ... ok
test password::tests::test_resolve_password_value_accepted_with_opt_in ... ok
test password::tests::test_resolve_password_env_var ... ok
test password::tests::test_resolve_password_none ... ok
test result: ok. 8 passed; 0 failed
```
### Integration Tests
```bash
# Test --password-stdin
$ echo "testpassword" | ./target/release/pdftract extract --password-stdin -
Password provided via secure channel
# ✅ PASS
# Test PDFTRACT_PASSWORD
$ PDFTRACT_PASSWORD="envpass" ./target/release/pdftract extract -
Password provided via secure channel
# ✅ PASS
# Test --password VALUE rejected (no opt-in)
$ ./target/release/pdftract extract --password secret -
Error: --password VALUE is insecure and rejected by default...
Exit code: 64
# ✅ PASS
# Test --password VALUE with opt-in (warning)
$ PDFTRACT_INSECURE_CLI_PASSWORD=1 ./target/release/pdftract extract --password secret -
WARNING: --password VALUE is insecure (visible via 'ps aux')...
Password provided via secure channel
# ✅ PASS
```
## Implementation Notes
### Stdin Discipline
- When `--password-stdin` is set with `-` (stdin input), the first line is the password and the rest is the PDF bytes
- Newline stripping: trims trailing `\n` or `\r\n` only; preserves leading/trailing whitespace
- Empty password (bare newline) is treated as "no password"
### Future Work
The following will be implemented in future beads when the respective crates are created:
- `pdftract-py/src/lib.rs`: PyO3 `password=` kwarg to `SecretString`
- `pdftract-mcp/src/handlers/extract.rs`: Password parameter in request body
- `pdftract-serve/src/routes/extract.rs`: Multipart form field for password
## References
- Plan: line 878 (TH-07 mitigation), line 902-913 (Secrets Handling), line 949 (NEVER log passwords)