README file from
GithubObsidian Cloud KMS Encryption
An Obsidian plugin providing transparent encryption of secret blocks and binary files using AWS KMS.
Table of Contents
- Why
- Key Principles
- How It Works
- Commands
- Usage
- Behavior
- Installation
- Settings
- Security
- Security Testing
- Reproducible Builds
- Verifying Release Integrity
- On-Disk Format
- Key Access Management
- CLI Decryption (without Obsidian)
- Comparison with Alternatives
- Git Workflow
- KMS Status Indicator
- Known Limitations
- Development
- License
Why
If you store your Obsidian vault in S3, Git, or any other remote storage — note contents are accessible to anyone who gains access to that storage. This plugin implements a Zero Trust Storage model: only ciphertext exists on disk and in remote. Decryption happens locally, in memory, only when Cloud KMS access is available.
Key Principles
- Envelope Encryption — each block/file is encrypted with a unique DEK (AES-256-GCM), and the DEK itself is wrapped by a CMK in cloud KMS
- Identity-based Auth — no passwords; uses system credentials (AWS SSO, IAM Role,
~/.aws/credentials) - Local-First Crypto — symmetric encryption runs locally via WebCrypto API; only the DEK goes to KMS for wrap/unwrap
- Zero Cleartext on Disk — decrypted content exists only in Obsidian process memory
- Transparent — encryption/decryption happens automatically on file read/write (monkey-patch vault adapter)
- Nested Content —
%%secret-start%%/%%secret-end%%markers don't conflict with code fences, allowing nestedmermaid,js and any other markdown
How It Works
Markdown files (secret blocks)
The plugin intercepts file reads and writes at the Obsidian vault adapter level:
- On write to disk: all blocks between
%%secret-start%%and%%secret-end%%are automatically encrypted → stored on disk asocke-v1block - On read from disk: all
ocke-v1blocks are automatically decrypted → shown in editor between%%secret-start%%/%%secret-end%%
Binary files (PDF, images, audio)
- "Encrypt current file" command encrypts the file in place (name unchanged)
- On open — file is decrypted in memory (Blob URL), Obsidian displays it normally
- Disk always contains encrypted bytes in OCKE format
- Encrypted files are marked with 🔒 in the file explorer
Commands
| Command | Description |
|---|---|
| Wrap selection in secret block | Wraps selected text in %%secret-start%% / %%secret-end%% |
| Unwrap secret block | Removes encryption markers, leaving plaintext |
| Encrypt current file with AWS KMS | Encrypts a binary file (PDF, PNG, MP3) in place |
| Decrypt current file with AWS KMS (permanent) | Permanently decrypts a binary file (writes plaintext to disk) |
Usage
Encrypting text in notes
- Select text in a note
Ctrl+P→ "Wrap selection in secret block"- Text is wrapped in
%%secret-start%%/%%secret-end%%markers - On save — automatically encrypted on disk
Creating a secret block manually
Simply wrap text in markers:
# My note
This is public text.
%%secret-start%%
This is secret content — will be encrypted on save.
Passwords, tokens, private notes — anything.
%%secret-end%%
This is public text again.
Nested code fences (mermaid, code, etc.)
%% markers are Obsidian comments, invisible in Reading view. Content between them is regular markdown that renders normally:
%%secret-start%%
# Secret Architecture
```mermaid
graph TD
A[Client] --> B[API Gateway]
B --> C[Lambda]
C --> D[DynamoDB]
```
```bash
export SECRET_KEY="my-super-secret-key"
aws s3 cp secret.tar.gz s3://my-bucket/
```
Production password: `P@ssw0rd123!`
%%secret-end%%
After saving, the entire block (including mermaid diagram and code) is encrypted on disk. On open — decrypted, and mermaid renders as a diagram in Reading view.
Encrypting binary files
- Open a PDF, image, or other binary file
Ctrl+P→ "Encrypt current file with AWS KMS"- File is encrypted in place (name unchanged, 🔒 appears in file explorer)
- On next open — decrypted in memory, displayed normally
For permanent decryption (write plaintext back to disk):
Ctrl+P→ "Decrypt current file with AWS KMS (permanent)"
Removing encryption
- Select the entire block (from
%%secret-start%%to%%secret-end%%) Ctrl+P→ "Unwrap secret block"- Markers are removed, text remains as regular markdown (no longer encrypted)
Behavior
| Situation | Result |
|---|---|
Saving .md with %%secret-start%% blocks |
Blocks encrypted → ocke-v1 block on disk |
Opening .md with ocke-v1 blocks (key available) |
Decrypted → %%secret-start%%...%%secret-end%% in editor |
Opening .md with ocke-v1 blocks (key NOT available) |
Remain as ocke-v1 (encrypted base64) |
| Opening encrypted PDF/PNG (key available) | Decrypted in memory → displayed normally |
| Opening encrypted PDF/PNG (key NOT available) | Obsidian cannot render the file |
| KMS unavailable on save | File saved as-is, error shown |
| Each block/file | Encrypted independently (own DEK) |
| File explorer | Encrypted binary files marked with 🔒 |
Installation
Requirements
- Obsidian ≥ 1.4.0 (desktop)
- AWS credentials configured (
~/.aws/credentialsoraws sso login)
From GitHub Releases
- Go to Releases
- Download from the latest release:
main.js,manifest.json,styles.css - Create folder
.obsidian/plugins/cloud-kms-encryption/in your vault - Place downloaded files in that folder
- Restart Obsidian → Settings → Community Plugins → enable "Cloud KMS Encryption"
From source
git clone https://github.com/ViktorUJ/obsidian-cloud-kms.git
cd obsidian-cloud-kms
npm install
npm run build
Copy main.js, manifest.json, and styles.css to .obsidian/plugins/cloud-kms-encryption/.
AWS Setup
-
Create a KMS key:
aws kms create-key --key-spec SYMMETRIC_DEFAULT --key-usage ENCRYPT_DECRYPT --region eu-north-1 -
Copy the key ARN (format:
arn:aws:kms:{region}:{account}:key/{key-id}) -
In Obsidian: Settings → Cloud KMS Encryption → paste the ARN
-
Verify credentials are available:
aws sts get-caller-identity
Note: region is extracted from the ARN automatically — no need to configure
AWS_REGION.
Settings
| Parameter | Description | Default |
|---|---|---|
| AWS KMS Key ARN | Key ARN for encryption | — |
| Auto-decrypt blocks | Automatic decryption on read | ✅ |
Security
Detailed threat model, cryptographic design, and limitations are described in SECURITY.md.
Key points:
- Decrypted data is never written to disk — adapter patch encrypts before write
- Binary files are decrypted to Blob URL (RAM), not to disk
- DEK is zeroed immediately after use
- Each block/file uses a unique DEK + nonce
- AES-256-GCM with 96-bit nonce and 128-bit auth tag
- Encryption context bound to vault name + file path + format version
- All KMS calls are logged in AWS CloudTrail
- LRU cache of 20 decrypted binary files (old ones evicted from memory)
- No telemetry, no external calls except to AWS KMS
- Credentials are not stored by the plugin — standard AWS credential chain is used
Verifying Release Integrity
Every release is cryptographically signed. You can verify that downloaded artifacts are authentic and untampered:
Verify SLSA Provenance
# Install GitHub CLI if not already
# Then verify the artifact was built in GitHub Actions:
gh attestation verify main.js --repo ViktorUJ/obsidian-cloud-kms
Verify Cosign Signature
# Install cosign: https://docs.sigstore.dev/cosign/system_config/installation/
# Download main.js and main.js.bundle from the release, then:
cosign verify-blob main.js \
--bundle main.js.bundle \
--certificate-identity-regexp "github.com/ViktorUJ/obsidian-cloud-kms" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
If verification succeeds — the file was built in this repository's GitHub Actions, not modified after build.
Verify SBOM
Each release includes sbom.spdx.json — a complete list of all bundled dependencies with versions and licenses. Use it to:
- Scan for known vulnerabilities:
grype sbom.spdx.json - Check license compliance:
trivy sbom sbom.spdx.json
On-Disk Format
Markdown (secret blocks)
On disk, secret blocks are stored as:
````ocke-v1
<base64-encoded encrypted data>
````
Binary files
The file is entirely replaced with OCKE binary format:
[Magic: "OCKE" 4B][Version: uint16 BE][ProviderIdLen: 1B][ProviderId]
[CmkIdLen: uint16 BE][CmkId][WrappedDekLen: uint16 BE][WrappedDek]
[Nonce: 12B][AuthTag: 16B][CiphertextLen: uint32 BE][Ciphertext]
Key Access Management
Multi-Key Architecture
In organizations, different teams need access to different secrets. This plugin supports multiple KMS keys, allowing fine-grained access control:
Use case: A company vault shared across teams:
- Finance team — access to budget data, salaries, contracts
- R&D team — access to patents, research, technical secrets
- CTO — access to everything (all keys)
Each secret block is encrypted with a specific key. IAM policies on the AWS side control who can decrypt what. A developer from R&D physically cannot decrypt finance data — even if they have access to the vault files.
Plugin settings:
{
"keys": [
{ "alias": "finance", "arn": "arn:aws:kms:eu-north-1:790660747904:key/aaa-111" },
{ "alias": "rnd", "arn": "arn:aws:kms:eu-north-1:790660747904:key/bbb-222" },
{ "alias": "cto", "arn": "arn:aws:kms:eu-north-1:790660747904:key/ccc-333" }
],
"defaultKeyAlias": "finance"
}
In notes — specify key alias in the marker:
%%secret-start:finance%%
Q3 Budget: $2.4M
Salaries: ...
%%secret-end%%
%%secret-start:rnd%%
Patent application for new algorithm: ...
%%secret-end%%
%%secret-start:cto%%
Production root credentials: ...
%%secret-end%%
Behavior:
- When encrypting: if multiple keys are configured, a picker appears to choose which key to use
- When decrypting: the key ARN is stored inside the encrypted data — the plugin automatically uses it
- If the user has IAM access to the key → block is decrypted
- If not → block remains encrypted (graceful degradation)
- A single note can contain blocks encrypted with different keys
IAM setup on AWS side:
- Finance team → IAM policy allows only
kms:Decryptonkey/aaa-111 - R&D team → IAM policy allows only
kms:Decryptonkey/bbb-222 - CTO → IAM policy allows
kms:Decrypton all three keys
User from the same AWS account
Add an IAM policy to the user/role:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "arn:aws:kms:eu-north-1:790660747904:key/YOUR-KEY-ID"
}
]
}
aws iam put-user-policy \
--user-name colleague \
--policy-name kms-vault-access \
--policy-document file://policy.json
For read-only access (decryption only) — remove kms:GenerateDataKey.
User from the same AWS Organization (different account)
Step 1. Update the Key Policy on the key owner's side — allow access from another account:
{
"Sid": "AllowCrossAccountDecrypt",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:root"
},
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "*"
}
# Get current key policy
aws kms get-key-policy --key-id YOUR-KEY-ID --policy-name default --output text > key-policy.json
# Add the Statement above to key-policy.json, then:
aws kms put-key-policy --key-id YOUR-KEY-ID --policy-name default --policy file://key-policy.json
Step 2. On the other account's side (111122223333) — add IAM policy to the user:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "arn:aws:kms:eu-north-1:790660747904:key/YOUR-KEY-ID"
}
]
}
Both conditions are required: Key Policy allows the account, IAM Policy allows the user.
User from an external organization (external AWS account)
Similar to cross-account, but with additional restrictions via Condition:
Step 1. Key Policy — allow a specific user/role (not the entire account):
{
"Sid": "AllowExternalPartnerDecrypt",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::444455556666:user/partner-user"
},
"Action": [
"kms:Decrypt",
"kms:DescribeKey"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:EncryptionContext:vaultName": "shared-vault"
}
}
}
Recommendations for external partners:
- Specify a concrete Principal (user/role ARN), not account
root- Grant only
kms:Decrypt(withoutGenerateDataKey) — read-only- Use
Conditionwithkms:EncryptionContextto restrict access to a specific vault- Enable CloudTrail for auditing all key access
Step 2. Partner adds IAM policy on their side (same as cross-account above).
Step 3. Partner configures the plugin with the same key ARN and gains decryption access.
Verifying access
# As the user who was granted access:
aws kms describe-key --key-id arn:aws:kms:eu-north-1:790660747904:key/YOUR-KEY-ID
# If it returns key metadata — access is granted
# If AccessDeniedException — check Key Policy + IAM Policy
Comparison with Alternatives
| Feature | Cloud KMS Encryption | SOPS | git-crypt | Meld Encrypt | HashiCorp Vault |
|---|---|---|---|---|---|
| Encryption at rest | ✅ | ✅ | ✅ | ✅ | ✅ |
| No passwords | ✅ (IAM) | ✅ (IAM/PGP) | ✅ (GPG) | ❌ (password) | ✅ (tokens) |
| Per-block granularity | ✅ | ✅ | ❌ (whole file) | ✅ | N/A |
| Multi-key / multi-team | ✅ | ✅ | ✅ | ❌ | ✅ |
| Git-safe (ciphertext in repo) | ✅ | ✅ | ✅ | ✅ | N/A |
| Transparent edit (no manual decrypt) | ✅ | ❌ (CLI) | ✅ | ❌ (modal) | N/A |
| Binary file encryption | ✅ | ❌ | ✅ | ❌ | N/A |
| Obsidian integration | ✅ native | ❌ | ❌ | ✅ native | ❌ |
| Audit trail (CloudTrail) | ✅ | ✅ | ❌ | ❌ | ✅ |
| External audit / certification | ❌ | ❌ | ❌ | ❌ | ✅ |
| HSM-grade security | ❌ | ❌ | ❌ | ❌ | ✅ |
Best fit:
- Cloud KMS Encryption — team knowledge bases with IAM access control, DevOps secrets in Obsidian
- SOPS — CI/CD secrets in YAML/JSON files, GitOps workflows
- git-crypt — whole-file encryption in Git repos, developer workflows
- Meld Encrypt — personal password-protected notes, single-user
- HashiCorp Vault — production infrastructure secrets, compliance requirements
Git Workflow
This plugin is designed to work with Git-stored vaults. On disk, only ciphertext exists — safe to commit and push.
Initial setup
# Clone the vault
git clone [email protected]:your-org/team-vault.git
cd team-vault
# Open in Obsidian, configure the plugin with your KMS key ARN
# Ensure AWS credentials are available:
aws sts get-caller-identity
Daily workflow
# Pull latest changes (encrypted on disk)
cd /path/to/vault
git pull
# Open Obsidian — plugin decrypts blocks transparently
# Edit notes as usual — secret blocks show decrypted content
# Close Obsidian or just switch to terminal
# Commit and push (only ciphertext goes to Git)
git add -A
git status # verify: no plaintext in diff
git commit -m "update finance Q3 notes"
git push
Verifying no plaintext leaks to Git
# Check what's actually in the file on disk:
cat notes/budget.md
# You should see: ocke-v1 block with base64 — NOT plaintext
# Check diff before committing:
git diff --cached
# Encrypted blocks show as base64 changes, not readable text
# For binary files:
file attachments/report.pdf
# Should show: "data" (not "PDF document") — it's encrypted bytes
Team onboarding
# New team member:
# 1. Clone the vault
git clone [email protected]:your-org/team-vault.git
# 2. Configure AWS credentials
aws configure
# or: aws sso login --profile team
# 3. Install plugin in Obsidian, set the same KMS key ARN
# 4. IAM admin grants kms:Decrypt permission on the relevant key(s)
# 5. Open vault in Obsidian — blocks they have access to are decrypted
# Blocks they DON'T have access to remain as encrypted base64
Conflict resolution
# If Git shows merge conflict in an encrypted block:
# DON'T try to merge the base64 manually — it's binary data
# Option 1: Accept theirs or ours
git checkout --theirs notes/budget.md
# or
git checkout --ours notes/budget.md
# Option 2: Re-encrypt after resolving in Obsidian
# 1. Accept one version
# 2. Open in Obsidian, edit the decrypted content
# 3. Save — plugin re-encrypts with new DEK
# 4. Commit
.gitignore recommendations
# Obsidian workspace (contains open file state, not secrets)
.obsidian/workspace.json
.obsidian/workspace-mobile.json
# Plugin data (contains your KMS ARN — not secret, but personal)
.obsidian/plugins/obsidian-cloud-kms-encryption/data.json
# Never ignore these (they ARE the encrypted vault):
# !*.md
# !attachments/
Pre-commit hook: plaintext leak protection
If the plugin was disabled, credentials expired, or a file was edited outside Obsidian — %%secret-start%% markers might end up on disk unencrypted. A pre-commit hook prevents accidentally committing plaintext to Git:
# Install the hook
cp tools/pre-commit-hook.sh .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
What it does:
- On every
git commit, checks all staged.mdfiles - If any file contains
%%secret-start%%— blocks the commit - Shows which file has unencrypted content and how to fix it
Example output when plaintext is detected:
ERROR: Plaintext secret block found in staged file: notes/budget.md
The file contains %%secret-start%% markers which means
the encryption plugin did not encrypt before save.
Fix: Open the file in Obsidian with the plugin enabled,
save it, then stage again.
Commit blocked: plaintext secrets detected.
This is a safety net — if everything works correctly,
%%secret-start%%should never appear on disk (the adapter patch encrypts it toocke-v1block before write). The hook catches edge cases.
CLI Decryption (without Obsidian)
For disaster recovery, CI/CD pipelines, or backup verification — you can decrypt files without Obsidian using CLI tools.
Important: Encryption context (
vault-nameandfile-path) must match what was used during encryption. The vault name is the folder name of your Obsidian vault. The file path is vault-relative (e.g.,folder/note.md).
Option 1: Bash + AWS CLI + Python
Zero Node.js dependencies. Requires: aws CLI, python3, pip install cryptography.
# Decrypt a binary file (PDF, image)
./tools/ocke-decrypt.sh report.pdf --vault-name my-vault --file-path report.pdf -o decrypted-report.pdf
# Decrypt markdown blocks (prints to stdout)
./tools/ocke-decrypt.sh notes/secret.md --vault-name my-vault --file-path notes/secret.md
# Using environment variables
OCKE_VAULT_NAME="my-vault" OCKE_FILE_PATH="notes/secret.md" ./tools/ocke-decrypt.sh notes/secret.md -o decrypted.md
Option 2: Node.js CLI
Requires: Node.js >= 18, @aws-sdk/client-kms.
# Install dependencies (one time)
npm install @aws-sdk/client-kms @aws-sdk/credential-provider-ini
# Decrypt a binary file
node tools/ocke-decrypt.mjs report.pdf --vault-name my-vault --file-path report.pdf -o decrypted-report.pdf
# Decrypt markdown blocks
node tools/ocke-decrypt.mjs notes/secret.md --vault-name my-vault --file-path notes/secret.md
# Using environment variables
OCKE_VAULT_NAME="my-vault" OCKE_FILE_PATH="notes/secret.md" node tools/ocke-decrypt.mjs notes/secret.md -o decrypted.md
Parameters
| Parameter | Description | Required |
|---|---|---|
<file> |
Path to the encrypted file | Yes |
-o <output> |
Write output to file (default: stdout) | No |
--vault-name |
Obsidian vault name (folder name) | Yes |
--file-path |
Vault-relative file path used during encryption | Yes |
Or via environment variables: OCKE_VAULT_NAME, OCKE_FILE_PATH.
How to find the correct values:
vault-name— the folder name of your vault (e.g., if vault is at/home/user/my-vault/, the name ismy-vault)file-path— the path relative to vault root (e.g.,notes/secret.md,attachments/report.pdf)
When to use CLI
- Disaster recovery — Obsidian is unavailable, need to access encrypted data
- CI/CD pipelines — decrypt secrets during deployment without GUI
- Backup verification — confirm encrypted backups are valid
- Migration — bulk decrypt when moving away from the plugin
Key Rotation / Migration (ocke-rekey)
Re-encrypt an entire vault with a new KMS key — for migrating to a new AWS account, rotating keys, or switching regions. Only the wrapped DEK is re-encrypted (fast), ciphertext is unchanged.
# Install dependencies
npm install @aws-sdk/client-kms @aws-sdk/credential-provider-ini
# Dry run — see what would change
node tools/ocke-rekey.mjs /path/to/vault \
--new-key arn:aws:kms:eu-west-1:NEW_ACCOUNT:key/new-key-id \
--vault-name my-vault \
--dry-run
# Execute migration
node tools/ocke-rekey.mjs /path/to/vault \
--new-key arn:aws:kms:eu-west-1:NEW_ACCOUNT:key/new-key-id \
--vault-name my-vault
# Migrate only blocks encrypted with a specific old key
node tools/ocke-rekey.mjs /path/to/vault \
--new-key arn:aws:kms:eu-west-1:NEW_ACCOUNT:key/new-key-id \
--old-key arn:aws:kms:eu-north-1:OLD_ACCOUNT:key/old-key-id \
--vault-name my-vault
Requirements:
- AWS credentials with
kms:Decrypton the old key ANDkms:Encrypton the new key - Both keys must be accessible simultaneously during migration
- After migration, update the plugin settings with the new key ARN
Note: On Windows, use Git Bash or WSL for correct Unicode file path handling. Both tools use the same AWS credentials as the plugin (
~/.aws/credentials). The KMS key ARN is stored inside the encrypted data — no key configuration needed.
KMS Status Indicator
The plugin displays a connection status indicator in Obsidian's bottom status bar:
| Indicator | Meaning |
|---|---|
| 🔓 KMS | Connection OK — encryption/decryption available |
| 🔒 KMS ⚠️ | KMS unavailable — secret blocks will NOT be encrypted on save! |
| ⏳ KMS | Checking connection... |
Behavior:
- Checks KMS accessibility on plugin load
- Re-checks every 5 minutes in background
- Click the indicator to manually re-check
- Hover for detailed tooltip
Why this matters:
If KMS is unavailable (network issues, expired credentials, AWS outage), the plugin cannot encrypt %%secret-start%% blocks on save. The file will be saved with plaintext markers. The status indicator gives immediate visibility into this risk — if you see 🔒 ⚠️, do not save files with secret blocks until connectivity is restored.
Known Limitations
Vault Adapter Monkey-Patching
This plugin intercepts vault.adapter.read() and vault.adapter.write() to provide transparent encryption. This is the same approach used by gpgCrypt and is the only way to guarantee zero-plaintext-on-disk without Obsidian providing an official encryption API.
Potential conflicts with other plugins:
- If another plugin also patches
adapter.read()oradapter.write(), the two patches may conflict - The plugin chains correctly (calls the original method after processing), but order of initialization matters
- If you experience issues, try disabling other plugins that modify file I/O behavior
Mitigations:
- The patch only processes
.mdfiles for text encryption (binary files checked by magic bytes) - Files without
%%secret-start%%markers or OCKE magic bytes pass through unchanged (zero overhead) - On plugin unload, original adapter methods are fully restored
- The patch is transparent — other plugins reading/writing non-encrypted files are unaffected
If Obsidian updates break the plugin:
- Your data is safe — files remain encrypted on disk in documented OCKE format
- Use the CLI tools to decrypt without Obsidian
- The plugin will be updated to match new Obsidian internals
Development
npm test # Run tests
npm run build # Production build
npm run dev # Dev build (watch)
make ci # Full CI pipeline
License
MIT © Viktar Mikalayeu