diff --git a/manifest.json b/manifest.json index cd50e24..94edd8e 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "id": "cloud-kms-encryption", "name": "Cloud KMS Encryption", "version": "0.1.1", - "minAppVersion": "1.4.0", + "minAppVersion": "1.7.2", "description": "Transparent encryption of secret blocks and binary files using AWS KMS. Zero plaintext on disk.", "author": "Viktar Mikalayeu", "isDesktopOnly": true diff --git a/src/ui/encrypted-view.ts b/src/ui/encrypted-view.ts index 9837b01..6f6708d 100644 --- a/src/ui/encrypted-view.ts +++ b/src/ui/encrypted-view.ts @@ -96,15 +96,7 @@ export class EncryptedFileView extends ItemView { container.empty(); // Error banner - const banner = container.createEl('div', { - cls: 'encrypted-view-error-banner', - }); - banner.style.backgroundColor = 'var(--background-modifier-error)'; - banner.style.color = 'var(--text-on-accent)'; - banner.style.padding = '12px 16px'; - banner.style.borderRadius = '4px'; - banner.style.marginBottom = '16px'; - banner.style.fontWeight = '600'; + const banner = container.createEl('div', { cls: 'ocke-encrypted-view-banner' }); const errorIcon = banner.createEl('span'); errorIcon.textContent = '⚠ '; @@ -113,36 +105,17 @@ export class EncryptedFileView extends ItemView { errorText.textContent = this.errorMessage || 'Decryption failed'; if (this.filePath) { - const fileInfo = banner.createEl('div'); - fileInfo.style.marginTop = '4px'; - fileInfo.style.fontWeight = '400'; - fileInfo.style.fontSize = '0.9em'; + const fileInfo = banner.createEl('div', { cls: 'ocke-encrypted-view-banner-detail' }); fileInfo.textContent = `File: ${this.filePath}`; } // Raw content section - const contentSection = container.createEl('div', { - cls: 'encrypted-view-content', - }); + const contentSection = container.createEl('div'); - const heading = contentSection.createEl('h4'); + const heading = contentSection.createEl('h4', { cls: 'ocke-encrypted-view-heading' }); heading.textContent = 'Raw On-Disk Content (Base64)'; - heading.style.marginBottom = '8px'; - - const codeBlock = contentSection.createEl('pre', { - cls: 'encrypted-view-raw-content', - }); - codeBlock.style.whiteSpace = 'pre-wrap'; - codeBlock.style.wordBreak = 'break-all'; - codeBlock.style.padding = '12px'; - codeBlock.style.backgroundColor = 'var(--background-secondary)'; - codeBlock.style.borderRadius = '4px'; - codeBlock.style.fontSize = '0.85em'; - codeBlock.style.fontFamily = 'var(--font-monospace)'; - codeBlock.style.maxHeight = '70vh'; - codeBlock.style.overflow = 'auto'; - codeBlock.style.userSelect = 'text'; + const codeBlock = contentSection.createEl('pre', { cls: 'ocke-encrypted-view-code' }); const code = codeBlock.createEl('code'); code.textContent = this.rawContentBase64 || '(no content)'; } diff --git a/src/ui/file-explorer-badge.ts b/src/ui/file-explorer-badge.ts index 98fd1c7..725c376 100644 --- a/src/ui/file-explorer-badge.ts +++ b/src/ui/file-explorer-badge.ts @@ -1,90 +1,53 @@ /** * Adds a 🔒 badge to encrypted files in the file explorer. - * - * Scans vault for files with OCKE magic bytes and marks them - * with a CSS class that shows a lock icon via ::after pseudo-element. + * CSS is in styles.css (class: ocke-encrypted-file). */ import type { Plugin } from 'obsidian'; const ENCRYPTED_CLASS = 'ocke-encrypted-file'; -const STYLE_ID = 'ocke-encrypted-badge-style'; - -/** - * Set of file paths known to be encrypted. - */ const encryptedPaths = new Set(); -/** - * Install the file explorer badge system. - * Returns a cleanup function. - */ export function installFileExplorerBadge( plugin: Plugin, originalReadBinary: (path: string) => Promise ): () => void { - // Inject CSS for the badge - const style = document.createElement('style'); - style.id = STYLE_ID; - style.textContent = ` - .nav-file.${ENCRYPTED_CLASS} .nav-file-title-content::after { - content: ' 🔒'; - font-size: 0.8em; - opacity: 0.7; - } - `; - document.head.appendChild(style); - // Scan vault on layout ready plugin.app.workspace.onLayoutReady(async () => { await scanVaultForEncryptedFiles(plugin, originalReadBinary); - applyBadges(plugin); + applyBadges(); }); - // Re-apply badges when file explorer updates + // Re-apply badges periodically const interval = window.setInterval(() => { - applyBadges(plugin); + applyBadges(); }, 3000); plugin.register(() => { window.clearInterval(interval); }); - // Return cleanup return () => { window.clearInterval(interval); - const el = document.getElementById(STYLE_ID); - if (el) el.remove(); removeBadges(); }; } -/** - * Mark a file path as encrypted (call after encrypting). - */ export function markFileEncrypted(filePath: string): void { encryptedPaths.add(filePath); } -/** - * Unmark a file path as encrypted (call after decrypting). - */ export function markFileDecrypted(filePath: string): void { encryptedPaths.delete(filePath); } -/** - * Scan vault for encrypted binary files (check first 4 bytes for OCKE magic). - */ async function scanVaultForEncryptedFiles( plugin: Plugin, originalReadBinary: (path: string) => Promise ): Promise { const files = plugin.app.vault.getFiles(); - for (const file of files) { if (file.path.endsWith('.md')) continue; - try { const data = await originalReadBinary(file.path); const bytes = new Uint8Array(data, 0, Math.min(4, data.byteLength)); @@ -97,19 +60,13 @@ async function scanVaultForEncryptedFiles( } } -/** - * Apply CSS class to file explorer items for encrypted files. - */ -function applyBadges(_plugin: Plugin): void { - const fileExplorer = document.querySelectorAll('.nav-file'); - +function applyBadges(): void { + const fileExplorer = activeDocument.querySelectorAll('.nav-file'); fileExplorer.forEach((el) => { const titleEl = el.querySelector('.nav-file-title'); if (!titleEl) return; - const path = titleEl.getAttribute('data-path'); if (!path) return; - if (encryptedPaths.has(path)) { el.classList.add(ENCRYPTED_CLASS); } else { @@ -118,11 +75,8 @@ function applyBadges(_plugin: Plugin): void { }); } -/** - * Remove all badges. - */ function removeBadges(): void { - document.querySelectorAll(`.${ENCRYPTED_CLASS}`).forEach((el) => { + activeDocument.querySelectorAll(`.${ENCRYPTED_CLASS}`).forEach((el) => { el.classList.remove(ENCRYPTED_CLASS); }); } diff --git a/src/ui/secret-block-processor.ts b/src/ui/secret-block-processor.ts index ad96615..9c97b41 100644 --- a/src/ui/secret-block-processor.ts +++ b/src/ui/secret-block-processor.ts @@ -1,14 +1,6 @@ /** * Markdown code block processor for ```ocke-v1 blocks. - * - * Registers a code block processor that renders encrypted blocks - * as visual widgets in both Reading view and Live Preview mode. - * - * Key principle: the actual document text is NEVER modified. - * The processor only controls how the block is rendered visually. - * - * - If decryption succeeds: shows 🔓 header + decrypted plaintext - * - If decryption fails: shows 🔒 header + truncated ciphertext + * Renders encrypted blocks as visual widgets in Reading view and Live Preview. */ import { Plugin, MarkdownPostProcessorContext } from 'obsidian'; @@ -17,17 +9,8 @@ import { decodeInlineBlock } from '../format/inline-codec'; import { parse } from '../format/parser'; import { FORMAT_VERSION } from '../constants'; -/** - * Cache of decrypted content keyed by base64 content. - */ const decryptionCache = new Map(); -/** - * Register the ocke-v1 code block processor. - * - * This makes Obsidian render ```ocke-v1 blocks with a custom visual - * instead of showing raw base64 text. - */ export function registerSecretBlockProcessor( plugin: Plugin, cryptoEngine: CryptoEngine, @@ -39,32 +22,10 @@ export function registerSecretBlockProcessor( async (source: string, el: HTMLElement, _ctx: MarkdownPostProcessorContext) => { const base64Content = source.trim(); - // Create container const container = el.createDiv({ cls: 'ocke-secret-block' }); - container.style.border = '1px solid var(--background-modifier-border)'; - container.style.borderRadius = '4px'; - container.style.overflow = 'hidden'; - - // Header const header = container.createDiv({ cls: 'ocke-secret-block-header' }); - header.style.padding = '4px 8px'; - header.style.fontSize = '0.8em'; - header.style.fontWeight = 'bold'; - header.style.display = 'flex'; - header.style.alignItems = 'center'; - header.style.gap = '4px'; - - // Content area const content = container.createEl('pre', { cls: 'ocke-secret-block-content' }); - content.style.padding = '8px'; - content.style.margin = '0'; - content.style.whiteSpace = 'pre-wrap'; - content.style.wordBreak = 'break-word'; - content.style.fontFamily = 'var(--font-monospace)'; - content.style.fontSize = '0.9em'; - content.style.background = 'var(--background-secondary)'; - // Check cache first const cached = decryptionCache.get(base64Content); if (cached) { if (cached.error) { @@ -75,14 +36,11 @@ export function registerSecretBlockProcessor( return; } - // Show loading state - header.style.background = 'var(--background-modifier-border)'; - header.style.color = 'var(--text-muted)'; + header.addClass('ocke-secret-block-header--loading'); header.textContent = '⏳ secret (decrypting...)'; content.textContent = '...'; - content.style.color = 'var(--text-muted)'; + content.addClass('ocke-secret-block-content--locked'); - // Attempt decryption try { const fullBlock = '```ocke-v1\n' + base64Content + '\n```'; const binaryData = decodeInlineBlock(fullBlock); @@ -114,30 +72,25 @@ export function registerSecretBlockProcessor( } function renderDecrypted(header: HTMLElement, content: HTMLElement, plaintext: string): void { - header.style.background = 'var(--interactive-accent)'; - header.style.color = 'var(--text-on-accent)'; + header.removeClass('ocke-secret-block-header--loading'); + header.removeClass('ocke-secret-block-header--locked'); + header.addClass('ocke-secret-block-header--decrypted'); header.textContent = '🔓 secret (decrypted)'; - + content.removeClass('ocke-secret-block-content--locked'); content.textContent = plaintext; - content.style.color = ''; } function renderLocked(header: HTMLElement, content: HTMLElement, base64Content: string): void { - header.style.background = 'var(--background-modifier-error)'; - header.style.color = 'var(--text-error)'; + header.removeClass('ocke-secret-block-header--loading'); + header.removeClass('ocke-secret-block-header--decrypted'); + header.addClass('ocke-secret-block-header--locked'); header.textContent = '🔒 secret (locked — no key access)'; - const preview = base64Content.length > 80 - ? base64Content.slice(0, 80) + '…' - : base64Content; + const preview = base64Content.length > 80 ? base64Content.slice(0, 80) + '…' : base64Content; + content.addClass('ocke-secret-block-content--locked'); content.textContent = preview; - content.style.color = 'var(--text-muted)'; - content.style.fontStyle = 'italic'; } -/** - * Clear the decryption cache. - */ export function clearDecryptionCache(): void { decryptionCache.clear(); } diff --git a/src/ui/settings-tab.ts b/src/ui/settings-tab.ts index 23872ab..681b493 100644 --- a/src/ui/settings-tab.ts +++ b/src/ui/settings-tab.ts @@ -55,22 +55,20 @@ export class CloudKmsSettingsTab extends PluginSettingTab { const { containerEl } = this; containerEl.empty(); - containerEl.createEl("h2", { text: "Cloud KMS Encryption Settings" }); + new Setting(containerEl).setName("Cloud KMS Encryption Settings").setHeading(); // --- Keys section --- - containerEl.createEl("h3", { text: "Encryption Keys" }); + new Setting(containerEl).setName("Encryption Keys").setHeading(); containerEl.createEl("p", { text: "Add KMS keys with aliases. Use aliases in %%secret-start:alias%% markers. The default key is used when no alias is specified.", cls: "setting-item-description", }); this.addKeysSection(containerEl); - - // --- Default key --- this.addDefaultKeySetting(containerEl); - // --- Legacy single key (backward compat) --- - containerEl.createEl("h3", { text: "Legacy Settings" }); + // --- Legacy --- + new Setting(containerEl).setName("Legacy Settings").setHeading(); containerEl.createEl("p", { text: "Used as fallback if no keys are configured above.", cls: "setting-item-description", @@ -78,7 +76,7 @@ export class CloudKmsSettingsTab extends PluginSettingTab { this.addArnSetting(containerEl); // --- Behavior --- - containerEl.createEl("h3", { text: "Behavior" }); + new Setting(containerEl).setName("Behavior").setHeading(); this.addAutoDecryptBlocksSetting(containerEl); } @@ -112,7 +110,7 @@ export class CloudKmsSettingsTab extends PluginSettingTab { this.plugin.settings.keys[index].alias = value.trim().toLowerCase().replace(/[^a-z0-9_-]/g, ''); await this.plugin.saveData(this.plugin.settings); }); - text.inputEl.style.width = "120px"; + text.inputEl.addClass('ocke-input-alias'); }) .addText((text) => { text @@ -122,7 +120,7 @@ export class CloudKmsSettingsTab extends PluginSettingTab { this.plugin.settings.keys[index].arn = value.slice(0, ARN_MAX_LENGTH); await this.plugin.saveData(this.plugin.settings); }); - text.inputEl.style.width = "300px"; + text.inputEl.addClass('ocke-input-arn'); }) .addButton((btn) => { btn.setButtonText("✕").setWarning().onClick(async () => { @@ -138,12 +136,10 @@ export class CloudKmsSettingsTab extends PluginSettingTab { // Validation if (key.arn && !validateAwsKmsArn(key.arn).valid) { - const errorEl = setting.controlEl.createEl("div", { + setting.controlEl.createEl("div", { text: "Invalid ARN format", - cls: "setting-error-message", + cls: "ocke-setting-error", }); - errorEl.style.color = "var(--text-error)"; - errorEl.style.fontSize = "0.8em"; } } @@ -185,18 +181,15 @@ export class CloudKmsSettingsTab extends PluginSettingTab { if (stripped.length > 0 && !validateAwsKmsArn(stripped).valid) { errorEl = setting.controlEl.createEl("div", { text: "Invalid AWS KMS key ARN format", - cls: "setting-error-message", + cls: "ocke-setting-error", }); - errorEl.style.color = "var(--text-error)"; - errorEl.style.fontSize = "0.85em"; - errorEl.style.marginTop = "4px"; } this.plugin.settings.awsCmkArn = clamped; await this.plugin.saveData(this.plugin.settings); }); text.inputEl.maxLength = ARN_MAX_LENGTH; - text.inputEl.style.width = "100%"; + text.inputEl.addClass('ocke-input-wide'); }); } diff --git a/src/ui/status-bar.ts b/src/ui/status-bar.ts index 602ef39..1a83923 100644 --- a/src/ui/status-bar.ts +++ b/src/ui/status-bar.ts @@ -46,21 +46,22 @@ export function installStatusBar( }; const setStatus = (status: 'ok' | 'error' | 'checking', detail: string) => { + statusBarEl.removeClass('ocke-status-bar--ok', 'ocke-status-bar--error', 'ocke-status-bar--checking'); switch (status) { case 'ok': statusBarEl.setText('🔓 KMS'); statusBarEl.setAttribute('title', 'KMS connection OK — encryption/decryption available'); - statusBarEl.style.color = ''; + statusBarEl.addClass('ocke-status-bar--ok'); break; case 'error': statusBarEl.setText('🔒 KMS ⚠️'); statusBarEl.setAttribute('title', `KMS unavailable: ${detail}\nSecret blocks will NOT be encrypted on save!`); - statusBarEl.style.color = 'var(--text-error)'; + statusBarEl.addClass('ocke-status-bar--error'); break; case 'checking': statusBarEl.setText('⏳ KMS'); statusBarEl.setAttribute('title', 'Checking KMS connection...'); - statusBarEl.style.color = 'var(--text-muted)'; + statusBarEl.addClass('ocke-status-bar--checking'); break; } }; @@ -72,7 +73,7 @@ export function installStatusBar( // Initial check after layout ready plugin.app.workspace.onLayoutReady(() => { - setTimeout(checkStatus, 2000); + window.setTimeout(checkStatus, 2000); }); return () => { diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..757b0db --- /dev/null +++ b/styles.css @@ -0,0 +1,121 @@ +/* Cloud KMS Encryption Plugin Styles */ + +/* Secret block processor & widget */ +.ocke-secret-block { + border: 1px solid var(--background-modifier-border); + border-radius: 4px; + margin: 4px 0; + overflow: hidden; +} + +.ocke-secret-block-header { + padding: 4px 8px; + font-size: 0.8em; + font-weight: bold; + display: flex; + align-items: center; + gap: 4px; +} + +.ocke-secret-block-header--decrypted { + background: var(--interactive-accent); + color: var(--text-on-accent); +} + +.ocke-secret-block-header--locked { + background: var(--background-modifier-error); + color: var(--text-error); +} + +.ocke-secret-block-header--loading { + background: var(--background-modifier-border); + color: var(--text-muted); +} + +.ocke-secret-block-content { + padding: 8px; + margin: 0; + white-space: pre-wrap; + word-break: break-word; + font-family: var(--font-monospace); + font-size: 0.9em; + background: var(--background-secondary); +} + +.ocke-secret-block-content--locked { + color: var(--text-muted); + font-style: italic; +} + +/* Encrypted file view */ +.ocke-encrypted-view-banner { + background-color: var(--background-modifier-error); + color: var(--text-on-accent); + padding: 12px 16px; + border-radius: 4px; + margin-bottom: 16px; + font-weight: 600; +} + +.ocke-encrypted-view-banner-detail { + margin-top: 4px; + font-weight: 400; + font-size: 0.9em; +} + +.ocke-encrypted-view-heading { + margin-bottom: 8px; +} + +.ocke-encrypted-view-code { + white-space: pre-wrap; + word-break: break-all; + padding: 12px; + background-color: var(--background-secondary); + border-radius: 4px; + font-size: 0.85em; + font-family: var(--font-monospace); + max-height: 70vh; + overflow: auto; + user-select: text; +} + +/* File explorer badge */ +.nav-file.ocke-encrypted-file .nav-file-title-content::after { + content: ' 🔒'; + font-size: 0.8em; + opacity: 0.7; +} + +/* Settings errors */ +.ocke-setting-error { + color: var(--text-error); + font-size: 0.85em; + margin-top: 4px; +} + +/* Status bar */ +.ocke-status-bar--ok { + color: var(--text-normal); +} + +.ocke-status-bar--error { + color: var(--text-error); +} + +.ocke-status-bar--checking { + color: var(--text-muted); +} + +/* Settings inputs */ +.ocke-input-alias { + width: 120px; +} + +.ocke-input-arn { + width: 300px; +} + +.ocke-input-wide { + width: 100%; +}