Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/contentScripts/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,22 @@ async function shakeInput(input, placeholderMsg) {
input.focus();
}

function getLocalizedString(key, fallback) {
const source = document.getElementById("sn-md");
return source?.dataset?.[key] || fallback;
}

// Password handle function
async function handleSubmit() {
const csID = document.getElementById("data-contentscript-id").innerText;
const input = document.getElementById("md-lock-input");
const password = input?.value?.trim() ?? "";

if (!password) {
await shakeInput(input, "Password cannot be empty");
await shakeInput(
input,
getLocalizedString("passwordCannotBeEmpty", "Password cannot be empty"),
);
logger("Empty password");
return;
}
Expand Down Expand Up @@ -69,7 +77,10 @@ async function init() {
if (snMd) snMd.style.display = "flex";
if (input) {
input.value = "";
input.placeholder = "Enter Password to View Note";
input.placeholder = getLocalizedString(
"enterPasswordToViewNote",
"Enter Password to View Note",
);
input.focus();
}
}
Expand Down
25 changes: 19 additions & 6 deletions src/contentScripts/secureView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
* @description : SecureNotes MarkdownIt renderer (RTE-safe).
*/

import strings from "../localization";

const escapeHtmlAttribute = (value: string): string =>
value
.replace(/&/g, "&")
.replace(/"/g, """)
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");

export default function (context: any) {
const contentScriptId = context.contentScriptId;
return {
Expand Down Expand Up @@ -34,7 +43,12 @@ export default function (context: any) {
const escaped = markdownIt.utils.escapeHtml(content);

return `
<div id="sn-md" class="sn-md joplin-editable">
<div
id="sn-md"
class="sn-md joplin-editable"
data-password-cannot-be-empty="${escapeHtmlAttribute(strings.passwordCannotBeEmpty)}"
data-enter-password-to-view-note="${escapeHtmlAttribute(strings.enterPasswordToViewNote)}"
>
<pre
class="joplin-source"
data-joplin-language="SecureNotes"
Expand All @@ -43,21 +57,20 @@ export default function (context: any) {
>${escaped}</pre>
<div id="md-lock" class="md-lock">
<h1 id="md-lock-title" class="md-lock-title">🔒 Secure Notes</h1>
<p id="md-lock-info" class="md-lock-info">This is an encrypted note</p>
<p id="md-lock-info" class="md-lock-info">${strings.secureViewEncryptedNoteInfo}</p>
<form id="md-lock-form" class="md-lock-form">
<input
id="md-lock-input"
type="password"
placeholder="Enter Password to View Note"
placeholder="${escapeHtmlAttribute(strings.enterPasswordToViewNote)}"
autocomplete="off"
/>
<button type="button" id="md-lock-btn">Unlock</button>
<button type="button" id="md-lock-btn">${strings.secureViewUnlockButton}</button>
</form>
</div>
<div id="md-unlock" class="md-unlock">
<div id="md-unlock-info" class="md-unlock-info">
🔒 This note is read-only. To edit it, decrypt the note, make changes,
then re-encrypt.
${strings.secureViewReadOnlyInfo}
</div>
<div id="md-unlock-box" class="md-unlock-box">
<div id="md-unlock-content" class="md-unlock-content"></div>
Expand Down
70 changes: 43 additions & 27 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ import {
decryptData,
} from "./encryption";
import { createLogger } from "./pluginLogger";
import strings, { setLocale } from "./localization";

/** Global constants */
export const PLUGIN_ID = "SecureNotes";
export const LOG_LEVEL = "DEBUG";
const JOPLIN_LOCALE_SETTING = "locale";

export const SETTINGS_SECTION = {
MAIN: `${PLUGIN_ID}.settings`,
Expand Down Expand Up @@ -83,9 +85,11 @@ const logger = createLogger(`[${PLUGIN_ID}]`, LOG_LEVEL);
*/
joplin.plugins.register({
onStart: async () => {
await initializeLocale();

// Register settings section
await joplin.settings.registerSection(SETTINGS_SECTION.MAIN, {
label: "Secure Notes",
label: strings.settingsSectionLabel,
iconName: "fas fa-user-shield",
});

Expand All @@ -96,47 +100,47 @@ joplin.plugins.register({
type: SettingItemType.Int,
section: SETTINGS_SECTION.MAIN,
public: true,
label: "AES Key Size",
label: strings.aesKeySizeLabel,
isEnum: true,
options: {
128: "128-bit",
256: "256-bit (Recommended)",
256: `256-bit (${strings.recommendedOptionSuffix})`,
},
},
[SETTINGS_MAIN.AES_MODE]: {
value: "AES-GCM",
type: SettingItemType.String,
section: SETTINGS_SECTION.MAIN,
public: true,
label: "AES Cipher Mode",
label: strings.aesCipherModeLabel,
isEnum: true,
options: {
"AES-CBC": "CBC",
"AES-CTR": "CTR",
"AES-GCM": "GCM (Recommended)",
"AES-GCM": `GCM (${strings.recommendedOptionSuffix})`,
},
},
});

// Register commands
await joplin.commands.register({
name: COMMANDS.ENCRYPT,
label: "Encrypt Note",
label: strings.encryptNoteCommand,
enabledCondition: "oneNoteSelected",
execute: encryptNote,
iconName: "fas fa-lock",
});
await joplin.commands.register({
name: COMMANDS.DECRYPT,
label: "Decrypt Note",
label: strings.decryptNoteCommand,
enabledCondition: "oneNoteSelected",
execute: decryptNote,
iconName: "fas fa-unlock",
});
await joplin.commands.register({
name: COMMANDS.TOGGLELOCK,
enabledCondition: "oneNoteSelected",
label: "Toggle Note Lock",
label: strings.toggleNoteLockCommand,
execute: toggleLock,
iconName: "fas fa-user-lock",
});
Expand Down Expand Up @@ -211,6 +215,18 @@ joplin.plugins.register({
/**
* Update global vars based on settings change.
*/
async function initializeLocale() {
try {
const locale = await joplin.settings.globalValue(JOPLIN_LOCALE_SETTING);

if (typeof locale === "string" && locale.trim()) {
setLocale(locale);
}
} catch (error) {
logger.warn("Failed to initialize locale:", error);
}
}

async function updateSettings() {
const pluginSettings = await joplin.settings.values([
SETTINGS_MAIN.KEY_SIZE,
Expand Down Expand Up @@ -267,8 +283,8 @@ export async function handlePasswdSubmit(passwd: string) {

if (!parsed) {
logger.error("Invalid format");
await showToast("Invalid format", ToastType.Error);
return { type: "error", msg: "Invalid format" };
await showToast(strings.invalidFormat, ToastType.Error);
return { type: "error", msg: strings.invalidFormat };
}

try {
Expand All @@ -287,11 +303,11 @@ export async function handlePasswdSubmit(passwd: string) {
} catch (error) {
if (error instanceof WrongPasswordError) {
logger.info("Incorrect password");
return { type: "error", msg: "Incorrect password, try again" };
return { type: "error", msg: strings.incorrectPasswordTryAgain };
}
logger.error("Decryption error:", error);
showToast("Decryption failed", ToastType.Error);
return { type: "error", msg: "Decryption failed" };
showToast(strings.decryptionFailed, ToastType.Error);
return { type: "error", msg: strings.decryptionFailed };
}
}

Expand All @@ -306,13 +322,13 @@ export async function encryptNote(note: any) {

if (isLocked) {
logger.debug("Note is already encrypted");
await showToast("Note is already encrypted", ToastType.Info);
await showToast(strings.noteIsAlreadyEncrypted, ToastType.Info);
return;
}

const passwd = await showEncryptionDialog(
encryptionDialogId,
"Enter password to Encrypt",
strings.enterPasswordToEncrypt,
);
if (!passwd) {
logger.debug("Password dialog cancelled");
Expand All @@ -324,7 +340,7 @@ export async function encryptNote(note: any) {
body: await generateEncryptedNote(aesOptions, encryptedData),
});

await showToast("Note encrypted successfully", ToastType.Success);
await showToast(strings.noteEncryptedSuccessfully, ToastType.Success);
logger.info("Encryption complete");
await refreshNoteView(note.id);
}
Expand All @@ -339,18 +355,18 @@ export async function decryptNote(note: any) {

if (!isLocked) {
logger.debug("Note is not encrypted");
await showToast("Note is not encrypted", ToastType.Info);
await showToast(strings.noteIsNotEncrypted, ToastType.Info);
return;
}

const parsed = await validateFormat(note.body);
if (!parsed) {
logger.error("Invalid format");
await showToast("Invalid format", ToastType.Error);
await showToast(strings.invalidFormat, ToastType.Error);
return;
}

let msg = "Enter password to Decrypt";
let msg = strings.enterPasswordToDecrypt;
// TODO: This is dangerous, limit it to 3 counts.
while (true) {
const passwd = await showDecryptionDialog(decryptionDialogId, msg);
Expand All @@ -368,17 +384,17 @@ export async function decryptNote(note: any) {
await joplin.data.put(["notes", note.id], null, {
body: decryptedContent,
});
await showToast("Note decrypted successfully", ToastType.Success);
await showToast(strings.noteDecryptedSuccessfully, ToastType.Success);
logger.info("Decryption complete");
await refreshNoteView(note.id);
return;
} catch (error) {
if (error instanceof WrongPasswordError) {
logger.info("Incorrect password");
msg = "Incorrect password, try again";
msg = strings.incorrectPasswordTryAgain;
} else {
logger.info("Decryption failed: ", error);
showToast("Decryption faild", ToastType.Error);
showToast(strings.decryptionFailed, ToastType.Error);
return;
}
}
Expand All @@ -395,11 +411,11 @@ export async function decryptOldNote(note: any) {
const parsed = validateOldFormat(note.body || "{}");
if (!parsed) {
logger.error("Invalid old format");
await showToast("Invalid old format", ToastType.Error);
await showToast(strings.invalidOldFormat, ToastType.Error);
return;
}

let msg = "Enter password to Decrypt";
let msg = strings.enterPasswordToDecrypt;

while (true) {
const passwd = await showDecryptionDialog(decryptionDialogId, msg);
Expand All @@ -415,17 +431,17 @@ export async function decryptOldNote(note: any) {
);
await joplin.data.put(["notes", note.id], null, { body: decrypted });
await removeTag(note.id, lockedTagId!);
await showToast("Note decrypted successfully", ToastType.Success);
await showToast(strings.noteDecryptedSuccessfully, ToastType.Success);
logger.info("Decryption complete:", note.id);
await refreshNoteView(note.id);
return;
} catch (error) {
if (error instanceof WrongPasswordError) {
logger.info("Incorrect password");
msg = "Incorrect password, try again";
msg = strings.incorrectPasswordTryAgain;
} else {
logger.info("Decryption failed: ", error);
showToast("Decryption faild", ToastType.Error);
showToast(strings.decryptionFailed, ToastType.Error);
return;
}
}
Expand Down
Loading