mirror of
https://github.com/viktoruj/obsidian-cloud-kms.git
synced 2026-07-22 06:56:21 +00:00
344 lines
17 KiB
Markdown
344 lines
17 KiB
Markdown
# Security Policy
|
|
|
|
## Threat Model
|
|
|
|
### Attacker Model
|
|
|
|
| Attacker | Capability | Access Level |
|
|
|----------|-----------|--------------|
|
|
| Cloud storage attacker | Has encrypted blobs only (S3 breach, Git repo leak) | Ciphertext only |
|
|
| Credential thief | Has stolen IAM credentials | Can call KMS if IAM allows |
|
|
| Local malware | Has filesystem access on user's machine | Can read encrypted files + potentially memory |
|
|
| Malicious Obsidian plugin | Has full Obsidian API access (DOM, editor, filesystem) | Can read decrypted content in editor |
|
|
| Network attacker (MITM) | Can intercept network traffic | TLS protects KMS calls |
|
|
| Physical access (powered off) | Has the device but no running session | Ciphertext only on disk |
|
|
| Physical access (powered on) | Has the device with active Obsidian session | Full access to decrypted content |
|
|
|
|
### Asset Classification
|
|
|
|
**Fully protected (encrypted at rest):**
|
|
- Note body content (inside `%%secret-start%%` blocks)
|
|
- Binary file content (PDF, images, audio)
|
|
- Data Encryption Keys (wrapped by KMS, zeroed after use)
|
|
|
|
**Partially protected:**
|
|
- File names (visible on disk, not encrypted)
|
|
- File timestamps (visible on disk)
|
|
- Vault structure / folder names (visible)
|
|
- Frontmatter metadata (visible, for Obsidian indexing)
|
|
|
|
**Not protected by this plugin:**
|
|
- AWS credentials (managed by OS/AWS CLI)
|
|
- Plugin settings (KMS key ARN — not a secret)
|
|
- Obsidian workspace state
|
|
|
|
### Security Assumptions
|
|
|
|
```
|
|
Assumes:
|
|
- Secure WebCrypto API implementation (browser/Electron engine)
|
|
- Uncompromised operating system and kernel
|
|
- Correct AWS IAM configuration (least privilege)
|
|
- TLS integrity for KMS API calls (AWS SDK handles this)
|
|
- No malicious Obsidian plugins installed in the same vault
|
|
- User does not disable the plugin and save files with secret markers
|
|
```
|
|
|
|
### Threat → Mitigation Matrix
|
|
|
|
| Threat | Mitigation | Residual Risk |
|
|
|--------|-----------|---------------|
|
|
| Stolen cloud storage (S3/Git) | Envelope encryption (AES-256-GCM + KMS) | None — ciphertext only |
|
|
| Man-in-the-middle | TLS (AWS SDK) + AEAD (GCM auth tag) | None |
|
|
| Ciphertext tampering | AES-256-GCM authentication tag | None — tamper detected |
|
|
| Key exposure | CMK never leaves AWS KMS HSM | None |
|
|
| DEK exposure in memory | Zero-fill after use, no caching | GC copies (JS limitation) |
|
|
| Replay / DEK reuse | Fresh random DEK + nonce per operation | None |
|
|
| Unauthorized decryption | IAM policies + KMS key policies + CloudTrail | Misconfiguration risk |
|
|
| Plugin disabled during save | Pre-commit hook blocks plaintext commits | User must install hook |
|
|
| KMS unavailable | Status bar indicator + graceful degradation | Plaintext in editor only (not on disk if plugin active) |
|
|
| Malicious plugin reads editor | No mitigation (Obsidian platform limitation) | Accept — documented |
|
|
| Memory dump | SecureBuffer zero-fill on release | JS heap copies (accept) |
|
|
| Supply chain attack | Pinned deps, SBOM, Scorecard, signed releases | Transitive dep risk remains |
|
|
|
|
### What this plugin protects against
|
|
|
|
| Threat | Protection |
|
|
|--------|-----------|
|
|
| Unauthorized access to vault storage (S3, Git, cloud sync) | ✅ Ciphertext-only on disk |
|
|
| Stolen laptop with powered-off device | ✅ Encrypted at rest |
|
|
| CloudTrail audit of who accessed secrets | ✅ Every KMS unwrap is logged |
|
|
| Accidental commit of secrets to Git | ✅ Only ciphertext in repo |
|
|
| Unauthorized KMS access | ✅ IAM policies + key policies |
|
|
|
|
### What this plugin does NOT protect against
|
|
|
|
| Threat | Reason |
|
|
|--------|--------|
|
|
| Malicious Obsidian plugin in same vault | Obsidian has no plugin sandbox — any plugin can read editor memory |
|
|
| Memory dump / cold boot attack | Plaintext exists in JS heap during editing session |
|
|
| Compromised workstation with active session | Attacker has same access as user |
|
|
| Keylogger / screen capture | Out of scope — OS-level threat |
|
|
| State-level adversary | Use dedicated HSM + isolated workstation instead |
|
|
|
|
### Trust Boundaries
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ User's Workstation │
|
|
│ │
|
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
│ │ Obsidian Process (Electron) │ │
|
|
│ │ │ │
|
|
│ │ ┌─────────────────────────────────────────────────────┐ │ │
|
|
│ │ │ Cloud KMS Plugin (this plugin) │ │ │
|
|
│ │ │ - Plaintext in editor buffer (TRUSTED) │ │ │
|
|
│ │ │ - DEK in memory briefly (TRUSTED) │ │ │
|
|
│ │ │ - Blob URLs for binary files (TRUSTED) │ │ │
|
|
│ │ │ - Adapter patch intercepts read/write │ │ │
|
|
│ │ └─────────────────────────────────────────────────────┘ │ │
|
|
│ │ │ │
|
|
│ │ ┌─────────────────────────────────────────────────────┐ │ │
|
|
│ │ │ Other Obsidian Plugins (UNTRUSTED) │ │ │
|
|
│ │ │ - Can access: DOM, editor API, filesystem │ │ │
|
|
│ │ │ - Can read: decrypted content in editor │ │ │
|
|
│ │ │ - Cannot access: KMS credentials directly │ │ │
|
|
│ │ └─────────────────────────────────────────────────────┘ │ │
|
|
│ │ │ │
|
|
│ └───────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
│ │ Local Filesystem (UNTRUSTED for plaintext) │ │
|
|
│ │ - Only ciphertext written │ │
|
|
│ │ - OCKE binary format / ````ocke-v1 base64 │ │
|
|
│ │ - File names and timestamps visible │ │
|
|
│ └───────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
│ │ OS Credential Store (~/.aws/credentials) │ │
|
|
│ │ - Managed by AWS CLI / SSO │ │
|
|
│ │ - Plugin reads, never writes │ │
|
|
│ └───────────────────────────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
│ │
|
|
│ TLS (HTTPS) │ git push (ciphertext)
|
|
▼ ▼
|
|
┌──────────────────────────────┐ ┌──────────────────────────────┐
|
|
│ AWS KMS (TRUSTED) │ │ Remote Storage (UNTRUSTED) │
|
|
│ - CMK in HSM │ │ - S3 / Git / Cloud Sync │
|
|
│ - Never exports key material │ │ - Only ciphertext │
|
|
│ - All ops in CloudTrail │ │ - No access to plaintext │
|
|
│ - IAM + Key Policy │ │ │
|
|
└──────────────────────────────┘ └──────────────────────────────┘
|
|
```
|
|
|
|
### Cryptographic Flow
|
|
|
|
```
|
|
ENCRYPTION (on file write):
|
|
┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────┐
|
|
│ Plaintext │────▶│ Generate │────▶│ AES-256-GCM │────▶│Ciphertext│
|
|
│ (editor) │ │ DEK+Nonce│ │ Encrypt │ │ + AuthTag│
|
|
└──────────┘ └────┬─────┘ └──────────────┘ └──────────┘
|
|
│
|
|
▼
|
|
┌──────────┐ ┌──────────────┐
|
|
│ DEK │────▶│ KMS Encrypt │────▶ Wrapped DEK
|
|
│(plaintext)│ │ (wrap) │ (safe to store)
|
|
└──────────┘ └──────────────┘
|
|
│
|
|
▼
|
|
Zero-fill DEK
|
|
|
|
DECRYPTION (on file read):
|
|
┌───────────┐ ┌──────────────┐ ┌──────────┐
|
|
│Wrapped DEK│────▶│ KMS Decrypt │────▶│ DEK │
|
|
│(from disk) │ │ (unwrap) │ │(plaintext)│
|
|
└───────────┘ └──────────────┘ └────┬─────┘
|
|
│
|
|
┌──────────┐ ┌──────────────┐ │
|
|
│Ciphertext│────▶│ AES-256-GCM │◀──────────┘
|
|
│ + AuthTag│ │ Decrypt+Verify│
|
|
└──────────┘ └──────┬───────┘
|
|
│
|
|
▼
|
|
┌──────────┐
|
|
│ Plaintext │────▶ Editor (memory only)
|
|
└──────────┘
|
|
│
|
|
Zero-fill DEK
|
|
```
|
|
|
|
## Cryptographic Design
|
|
|
|
### Envelope Encryption
|
|
|
|
Each secret block or binary file is encrypted with a unique Data Encryption Key (DEK):
|
|
|
|
1. **GenerateDataKey** → KMS returns plaintext DEK + wrapped DEK
|
|
2. **Encrypt** → AES-256-GCM(plaintext, DEK, nonce) → ciphertext + auth tag
|
|
3. **Store** → wrapped DEK + nonce + auth tag + ciphertext (DEK is zeroed)
|
|
4. **Decrypt** → KMS unwraps DEK → AES-256-GCM decrypt → plaintext (DEK is zeroed)
|
|
|
|
### Primitives
|
|
|
|
| Component | Choice | Rationale |
|
|
|-----------|--------|-----------|
|
|
| Symmetric cipher | AES-256-GCM | AEAD, hardware-accelerated, WebCrypto native |
|
|
| Nonce | 96-bit random | crypto.getRandomValues(), unique per encryption |
|
|
| Auth tag | 128-bit | GCM default, tamper detection |
|
|
| Key wrap | AWS KMS | HSM-backed, auditable, IAM-controlled |
|
|
| DEK size | 256-bit | AES-256 key length |
|
|
| Key derivation | None (KMS generates DEK directly) | No custom KDF needed |
|
|
|
|
### What we explicitly avoid
|
|
|
|
- ❌ Custom cryptographic algorithms
|
|
- ❌ CBC mode (no authentication)
|
|
- ❌ Deterministic IVs/nonces
|
|
- ❌ Hardcoded salts or keys
|
|
- ❌ Password-based encryption (PBKDF2, scrypt)
|
|
- ❌ Client-side key storage
|
|
|
|
## Memory Handling
|
|
|
|
### Secure cleanup
|
|
|
|
- `SecureBuffer` class: zero-fills buffer on release
|
|
- DEK zeroed immediately after encrypt/decrypt operation
|
|
- Blob URLs revoked on plugin unload
|
|
- LRU cache evicts old decrypted binary files (max 20)
|
|
|
|
### Known limitations
|
|
|
|
- JavaScript garbage collector may retain copies of plaintext
|
|
- Obsidian undo buffer contains decrypted text
|
|
- Electron renderer process holds editor state in memory
|
|
- OS swap/pagefile may contain memory pages with plaintext
|
|
|
|
## Credential Handling
|
|
|
|
### How credentials are loaded
|
|
|
|
```
|
|
AWS SDK credential chain:
|
|
1. ~/.aws/credentials (fromIni)
|
|
2. AWS SSO session
|
|
3. IAM instance role (EC2/ECS)
|
|
4. Environment variables
|
|
```
|
|
|
|
### What is NOT stored by the plugin
|
|
|
|
- ❌ AWS access keys
|
|
- ❌ AWS secret keys
|
|
- ❌ Session tokens
|
|
- ❌ Any credential material
|
|
|
|
### What IS stored in plugin settings
|
|
|
|
- ✅ KMS Key ARN (not a secret — it's a resource identifier)
|
|
- ✅ User preferences (auto-decrypt toggle, suffix)
|
|
|
|
## Logging
|
|
|
|
- Structured JSON logs via `structured-logger.ts`
|
|
- Sanitizer strips sensitive fields before logging
|
|
- No plaintext, DEK, or credential values in logs
|
|
- Only metadata: provider, file path, payload size, timing
|
|
|
|
## Supply Chain
|
|
|
|
- Dependencies pinned in `package-lock.json`
|
|
- `npm audit` runs in CI on every push
|
|
- Reproducible builds via Docker (`make docker-release`)
|
|
- No telemetry, no analytics, no network calls except to AWS KMS
|
|
|
|
## Reporting Vulnerabilities
|
|
|
|
If you discover a security vulnerability, please report it privately:
|
|
|
|
- Email: viktoruj@gmail.com
|
|
- Do NOT open a public GitHub issue for security vulnerabilities
|
|
- I will respond within 48 hours and work on a fix
|
|
|
|
## Recommended Usage
|
|
|
|
### Good fit
|
|
|
|
- Personal API keys and tokens
|
|
- Terraform/infrastructure secrets
|
|
- Internal documentation with sensitive data
|
|
- Developer secret workflows (similar to SOPS/git-crypt)
|
|
- Team knowledge bases with access-controlled secrets
|
|
|
|
### Consider alternatives for
|
|
|
|
- Production root credentials → use AWS Secrets Manager / HashiCorp Vault
|
|
- Regulated data (HIPAA, PCI-DSS) → use certified solutions with compliance attestation
|
|
- State-level adversary threat model → use hardware-backed auth + isolated workstation
|
|
|
|
## Secure Deployment Recommendations
|
|
|
|
### Dedicated Vault for Secrets
|
|
|
|
For maximum security, use a **separate Obsidian vault** for encrypted content:
|
|
|
|
```
|
|
~/vaults/
|
|
├── work/ ← General notes, many plugins installed
|
|
├── personal/ ← Personal notes
|
|
└── secrets/ ← Encrypted vault (minimal plugins)
|
|
```
|
|
|
|
**Why:** Other plugins in the same vault can read decrypted content from the editor. A dedicated vault minimizes this attack surface.
|
|
|
|
### Minimal Plugin Configuration
|
|
|
|
In the encrypted vault, install only:
|
|
- This plugin (Cloud KMS Encryption)
|
|
- Essential plugins you trust (e.g., built-in plugins only)
|
|
|
|
**Avoid** in the encrypted vault:
|
|
- Community plugins with network access
|
|
- Plugins that sync content to external services
|
|
- Plugins that read/modify file content
|
|
- Plugins from unknown/unverified authors
|
|
|
|
### Workstation Hardening
|
|
|
|
| Measure | Purpose |
|
|
|---------|---------|
|
|
| Full-disk encryption (BitLocker/FileVault) | Protects swap/pagefile containing memory pages |
|
|
| Screen lock on idle | Prevents physical access to active session |
|
|
| Disable swap/pagefile (if RAM allows) | Eliminates plaintext in swap |
|
|
| Use AWS SSO with short-lived tokens | Limits credential exposure window |
|
|
| Enable MFA on AWS account | Prevents credential theft from granting KMS access |
|
|
| Regular `aws sso logout` when done | Invalidates cached session |
|
|
|
|
### Network Security
|
|
|
|
| Measure | Purpose |
|
|
|---------|---------|
|
|
| VPN for KMS access | Ensures connectivity, prevents MITM on corporate networks |
|
|
| AWS VPC PrivateLink for KMS | Eliminates internet dependency for KMS calls |
|
|
| DNS-over-HTTPS | Prevents DNS-based traffic analysis |
|
|
|
|
### Monitoring
|
|
|
|
| What to monitor | How |
|
|
|-----------------|-----|
|
|
| KMS Decrypt calls | CloudTrail → CloudWatch alarm on unusual patterns |
|
|
| Failed KMS auth | CloudTrail → alert on AccessDeniedException |
|
|
| New IAM grants on KMS key | AWS Config rule |
|
|
| Plugin status indicator | Visual check: 🔓 = OK, 🔒 ⚠️ = problem |
|
|
|
|
### Incident Response
|
|
|
|
If you suspect compromise:
|
|
|
|
1. **Immediately:** `aws sso logout` or rotate IAM credentials
|
|
2. **Assess:** Check CloudTrail for unauthorized KMS Decrypt calls
|
|
3. **Contain:** Disable KMS key if unauthorized access confirmed (`aws kms disable-key`)
|
|
4. **Recover:** Re-enable key after investigation, rotate if needed (`tools/ocke-rekey.mjs`)
|
|
5. **Prevent:** Review IAM policies, enable MFA, audit plugin list
|