fix: use CSS classes instead of inline styles, bump minAppVersion to 1.7.2 [skip ci]

This commit is contained in:
Viktar Mikalayeu 2026-05-12 23:29:52 +04:00
parent 8f2bdb32e5
commit 0d7eb23a1c
7 changed files with 162 additions and 167 deletions

View file

@ -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

View file

@ -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)';
}

View file

@ -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<string>();
/**
* Install the file explorer badge system.
* Returns a cleanup function.
*/
export function installFileExplorerBadge(
plugin: Plugin,
originalReadBinary: (path: string) => Promise<ArrayBuffer>
): () => 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<ArrayBuffer>
): Promise<void> {
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);
});
}

View file

@ -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<string, { plaintext: string | null; error: boolean }>();
/**
* 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();
}

View file

@ -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');
});
}

View file

@ -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 () => {

121
styles.css Normal file
View file

@ -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%;
}