fix: validate SSH private keys for vault encryption#404
Conversation
Extract binary key material from PEM envelope instead of feeding raw text (including headers/footers) to PBKDF2. Reject encrypted SSH keys that would silently break vault secrets on passphrase change. Validate file permissions match SSH's own 0600 requirement. Bump EncryptedData.version to 2 for the new derivation format. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7dcf4f0 to
57df9fe
Compare
There was a problem hiding this comment.
Code Review Summary
This PR implements important security improvements for SSH key validation in vault encryption. The changes are well-structured, comprehensively tested, and follow the project's code style guidelines.
✅ No Blocking Issues
The PR passes all quality checks:
- TypeScript strict mode: No
anytypes - Named exports: Properly using named exports per CLAUDE.md
- License headers: Present in both files
- Test coverage: Comprehensive tests for all new functionality (encrypted key rejection, permission validation, fallback behavior)
- Security: Addresses multiple security concerns appropriately
DDD Review
The code properly implements a Domain Service pattern. LocalEncryptionVaultProvider encapsulates encryption logic and key management without leaking implementation details. The new validation methods (validateSshKeyPermissions, detectEncryptedPemKey, detectEncryptedOpenSshKey, extractPemKeyMaterial) are appropriately scoped as private helper methods. No DDD anti-patterns detected.
Security Improvements ✅
- Binary key extraction - Extracting binary key material from PEM envelope improves entropy usage
- Encrypted key rejection - Prevents silent failures when SSH key passphrases change
- Permission validation - Matches SSH's own security requirements (0o600)
- Windows handling - Sensibly skips POSIX permission checks on Windows
Suggestions (Non-blocking)
-
Backwards compatibility note: The key derivation change from raw PEM text to extracted binary means v1-encrypted secrets cannot be decrypted with this new code. The version bump to 2 appropriately marks this format change. If there's existing production data using v1 format, consider adding a migration note to the release documentation.
-
Minor edge case: In
readAndValidateSshKey()(line 298), ifHOMEenv var is undefined, the path expansion producesundefined/.ssh/id_rsa. This is an unlikely edge case since HOME is nearly always set, but could be handled with a clearer error message. -
Consider allowing 0o400: The permission check rejects anything with
(mode & 0o077) !== 0, which correctly blocks group/other access. SSH itself also allows read-only0o400permissions, which this code already accepts. Just confirming this is intentional (it is - the check is correct).
Code Quality Highlights
- Clean refactoring: Extracted duplicated SSH key reading into
readAndValidateSshKey() - Good error messages with actionable suggestions (e.g., "Run 'chmod 600 ...'")
- Defensive parsing in
detectEncryptedOpenSshKey()- silently allows unknown formats through rather than false-rejecting valid keys - Proper test isolation with
withTempDir()helper
LGTM! The security improvements are valuable and the implementation is solid.
Summary
Closes #397
0600requirementreadAndValidateSshKey()methodEncryptedData.versionto2to mark the new derivation formatTest plan
mode: 0o600deno check,deno lint,deno fmtall pass🤖 Generated with Claude Code