Viktar Mikalayeu22 downloadsTransparent encryption of secret blocks and binary files using AWS KMS. Zero plaintext on disk.
An Obsidian plugin providing transparent encryption of secret blocks and binary files using AWS KMS.
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.
~/.aws/credentials)%%secret-start%% / %%secret-end%% markers don't conflict with code fences, allowing nested mermaid, js and any other markdownThe plugin intercepts file reads and writes at the Obsidian vault adapter level:
%%secret-start%% and %%secret-end%% are automatically encrypted → stored on disk as ocke-v1 blockocke-v1 blocks are automatically decrypted → shown in editor between %%secret-start%% / %%secret-end%%| 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) |
Ctrl+P → "Wrap selection in secret block"%%secret-start%% / %%secret-end%% markersSimply 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.
%% 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.
Ctrl+P → "Encrypt current file with AWS KMS"For permanent decryption (write plaintext back to disk):
Ctrl+P → "Decrypt current file with AWS KMS (permanent)"%%secret-start%% to %%secret-end%%)Ctrl+P → "Unwrap secret block"| 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 🔒 |
~/.aws/credentials or aws sso login)main.js, manifest.json, styles.css.obsidian/plugins/cloud-kms-encryption/ in your vaultgit 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/.
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.
| Parameter | Description | Default |
|---|---|---|
| AWS KMS Key ARN | Key ARN for encryption | — |
| Auto-decrypt blocks | Automatic decryption on read | ✅ |
Detailed threat model, cryptographic design, and limitations are described in SECURITY.md.
Key points:
Every release is cryptographically signed. You can verify that downloaded artifacts are authentic and untampered:
# 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
# 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.
Each release includes sbom.spdx.json — a complete list of all bundled dependencies with versions and licenses. Use it to:
grype sbom.spdx.jsontrivy sbom sbom.spdx.jsonOn disk, secret blocks are stored as:
````ocke-v1
<base64-encoded encrypted data>
````
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]
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:
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:
IAM setup on AWS side:
kms:Decrypt on key/aaa-111kms:Decrypt on key/bbb-222kms:Decrypt on all three keysAdd 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.
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.
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.
# 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
| 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:
This plugin is designed to work with Git-stored vaults. On disk, only ciphertext exists — safe to commit and push.
# 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
# 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
# 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
# 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
# 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
# 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/
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:
git commit, checks all staged .md files%%secret-start%% — blocks the commitExample 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.
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).
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
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
| 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)
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:
kms:Decrypt on the old key AND kms:Encrypt on the new keyNote: 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.
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:
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.
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:
adapter.read() or adapter.write(), the two patches may conflictMitigations:
.md files for text encryption (binary files checked by magic bytes)%%secret-start%% markers or OCKE magic bytes pass through unchanged (zero overhead)If Obsidian updates break the plugin:
npm test # Run tests
npm run build # Production build
npm run dev # Dev build (watch)
make ci # Full CI pipeline
MIT © Viktar Mikalayeu