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>
93 lines
3.6 KiB
Markdown
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)
|