diff --git a/PerPageCrypto/PerPageCrypto.js b/PerPageCrypto/PerPageCrypto.js new file mode 100644 index 0000000..a6bf437 --- /dev/null +++ b/PerPageCrypto/PerPageCrypto.js @@ -0,0 +1,434 @@ +(() => { + const MASTER_PASSWORD = ''; // leave empty to disable master password check, otherwise use sha256 hash + const PASSWORD_SALT = 'mySecretSaltValue123!'; // CHANGE THIS + + const SimpleCrypto = require('SimpleCrypto.min.js'); + + class PadlockWidget extends api.NoteContextAwareWidget { + constructor() { + super(); + + this.$widget = $('
'); + + // Container for countdown + input to keep them aligned + this.$inputContainer = $('
'); + this.$widget.append(this.$inputContainer); + + // Countdown display (left of input) + this.$countdown = $('0:00'); + this.$inputContainer.append(this.$countdown); + this.$countdown.on('click', () => this.addTimeToActiveNote(300)); // add 5 minutes on click + + // Password input + this.$input = $('').css({ + display: 'inline-block', + width: '120px', + marginRight: '5px', + verticalAlign: 'middle', + padding: '3px 6px', + fontSize: '14px', + border: '1px solid #525252', + borderRadius: '4px', + boxSizing: 'border-box', + outline: 'none', + marginLeft:'20px' + }); + this.$inputContainer.append(this.$input); + + // Padlock button + this.$button = $(''); + this.$widget.append(this.$button); + + // Internal state + this.isBusy = false; + + // Per-note storage + this.notePasswords = {}; // noteId -> password + this.noteTimers = {}; // noteId -> intervalId + this.noteRemaining = {}; // noteId -> remaining seconds + + // Button click handler + this.$button.on('click', () => this._onClick()); + + // Inject CSS for widget styling + this.injectCss(); + } + + get position() { return 100; } + get parentWidget() { return 'note-detail-pane'; } + get styleId() { return 'padlock-widget-styles'; } + + doRender() { return this.$widget; } + + refreshWithNote() { + const toolbar = document.querySelector('.ribbon-button-container'); + if (!toolbar) return; + + const existing = toolbar.querySelector('.padlock-action'); + if (!existing) toolbar.prepend(this.$widget[0]); + + this.updatePadlockIcon(); + this.updateCountdownDisplayForActiveNote(); + } + + _onClick() { + if (this.isBusy) return; + + const note = api.getActiveContextNote(); + if (!note || !note.noteId) { + console.warn('[CryptoROM] No active note found'); + return; + } + + this.isBusy = true; + + const isLocked = note.hasLabel && note.hasLabel('CryptoRoM'); + try { + if (isLocked) { + this.decrypt(note); + } else { + this.encrypt(note); + } + } catch (err) { + console.error('[CryptoROM] Button click error:', err); + this.isBusy = false; + this.setLoading(false); + } +} + + setLoading(isLoading) { + this.isBusy = isLoading; + if (isLoading) { + this.$button.removeClass('bx-lock-alt bx-lock-open-alt') + .addClass('bx bx-loader-alt bx-spin') + .attr('title', 'Processing...'); + } + } + + updateNoteContent(noteId, manipulateFn, lockFlag) { + if (!noteId) return Promise.reject('No noteId'); + + return api.runOnBackend((noteId) => { + const note = api.getNote(noteId); + if (!note) return null; + return note.getContent(); + }, [noteId]).then((originalContent) => { + if (originalContent == null) throw new Error('Failed to read note content'); + const newContent = manipulateFn(originalContent); + + return api.runOnBackend((noteId, content, lockFlag) => { + const note = api.getNote(noteId); + if (!note) return; + note.setContent(content); + if (lockFlag) { + if (!note.hasLabel('CryptoRoM')) note.addLabel('CryptoRoM'); + } else { + if (note.hasLabel('CryptoRoM')) note.removeLabel('CryptoRoM'); + } + note.save(); + }, [noteId, newContent, lockFlag]); + }); + } + + encryptNoteId(noteId) { + if (!noteId) { + return; + } + + const note = api.getNote(noteId); + if (!note) { + console.warn('[CryptoROM] Could not fetch note for encryption', noteId); + return; + } + + const inputPassword = this.$input.val(); + if (inputPassword) { + this.notePasswords[noteId] = inputPassword; // store for future + } + + const password = this.notePasswords[noteId]; + if (!password) { + console.warn('[CryptoROM] No stored password for note', noteId); + return; + } + + this.setLoading(true); + this.updateNoteContent(noteId, content => { + + const saltedPassword = password + PASSWORD_SALT; + + const encrypted = new SimpleCrypto(saltedPassword).encrypt(content); + return encrypted; + }, true) + .then(() => { + this.updatePadlockIcon(); + if (inputPassword) this.$input.val(''); + }) + .finally(() => this.setLoading(false)); + } + + encrypt(note) { + const noteId = note.noteId; + + const inputPassword = this.$input.val(); + if (inputPassword) this.notePasswords[noteId] = inputPassword; + + const password = this.notePasswords[noteId]; + if (!password) { + console.warn('[CryptoROM] No password provided for encryption'); + this.$input.val(''); + this.isBusy = false; + return; + } + + const flashError = () => { + this.$input.addClass('crypto-input-error'); + setTimeout(() => this.$input.removeClass('crypto-input-error'), 600); + this.$input.val(''); + }; + + // Skip master check if empty + if (!MASTER_PASSWORD) { + this.performEncryption(noteId, password, inputPassword); + return; + } + + const parts = password.split(':'); + if (parts.length !== 2) { + flashError(); + this.isBusy = false; + return; + } + + const [masterPart, userPart] = parts; + const encoder = new TextEncoder(); + + crypto.subtle.digest('SHA-256', encoder.encode(masterPart)) + .then(hashBuffer => { + const hashHex = Array.from(new Uint8Array(hashBuffer)) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); + + if (hashHex !== MASTER_PASSWORD) { + console.warn('[CryptoROM] Master password mismatch'); + flashError(); + this.isBusy = false; + throw new Error('Master password incorrect'); + } + + // Verified, perform encryption + this.performEncryption(noteId, userPart, inputPassword); + }) + .catch(err => { + console.error('[CryptoROM] Encryption aborted:', err); + flashError(); + this.isBusy = false; + this.setLoading(false); + }); + } + + performEncryption(noteId, password, inputPassword) { + const note = api.getNote(noteId); + if (!note) { + console.error('[CryptoROM] No note found for encryption'); + return; + } + + this.clearTimer(noteId); + this.setLoading(true); + + this.updateNoteContent(noteId, content => { + const saltedPassword = password + PASSWORD_SALT; + const encrypted = new SimpleCrypto(saltedPassword).encrypt(content); + return encrypted; + }, true) + .then(() => { + this.updatePadlockIcon(); + if (inputPassword) this.$input.val(''); + delete this.notePasswords[noteId]; + }) + .finally(() => this.setLoading(false)); + } + + + + decrypt(note) { + const noteId = note.noteId; + + const password = this.$input.val(); + if (!password) { + alert('Please enter a password for decryption.'); + this.isBusy = false; + return; + } + + // Store password for timer-based re-encryption + this.notePasswords[noteId] = password; + this.setLoading(true); + + this.updateNoteContent(noteId, content => { + let decrypted; + try { + const saltedPassword = password + PASSWORD_SALT; + decrypted = new SimpleCrypto(saltedPassword).decrypt(content); + } catch (err) { + this.$input.val(''); // clear input + // Flash input red if decryption fails + this.$input.addClass('crypto-input-error'); + setTimeout(() => this.$input.removeClass('crypto-input-error'), 600); + throw err; + } + return decrypted; + }, false) + .then(() => { + this.updatePadlockIcon(); + this.$input.val(''); // clear input + + this.startTimer(noteId); + }) + .catch(err => { + this.updatePadlockIcon(); + }) + .finally(() => this.setLoading(false)); + } + + + startTimer(noteId) { + this.clearTimer(noteId); + + this.noteRemaining[noteId] = 600; // 10 minutes (in seconds) + this.updateCountdownDisplayForActiveNote(); + + this.noteTimers[noteId] = setInterval(() => { + this.noteRemaining[noteId]--; + this.updateCountdownDisplayForActiveNote(); + + if (this.noteRemaining[noteId] <= 0) { + this.clearTimer(noteId); + this.encryptNoteId(noteId); + } + }, 1000); + } + + clearTimer(noteId) { + if (this.noteTimers[noteId]) { + clearInterval(this.noteTimers[noteId]); + delete this.noteTimers[noteId]; + delete this.noteRemaining[noteId]; + this.updateCountdownDisplayForActiveNote(); + } + } + + + addTimeToActiveNote(seconds) { + const note = api.getActiveContextNote(); + if (!note || !note.noteId) return; + + const noteId = note.noteId; + if (this.noteRemaining[noteId] != null) { + this.noteRemaining[noteId] += seconds; + this.updateCountdownDisplayForActiveNote(); + } + } + + clearTimer(noteId) { + if (this.noteTimers[noteId]) { + clearInterval(this.noteTimers[noteId]); + delete this.noteTimers[noteId]; + delete this.noteRemaining[noteId]; + this.updateCountdownDisplayForActiveNote(); + } + } + + updateCountdownDisplayForActiveNote() { + const note = api.getActiveContextNote(); + if (!note || !note.noteId) { + this.$countdown.text(''); + return; + } + + const remaining = this.noteRemaining[note.noteId]; + if (remaining != null) { + const min = Math.floor(remaining / 60); + const sec = remaining % 60; + this.$countdown.text(`${min}:${sec.toString().padStart(2,'0')}`); + } else { + this.$countdown.text(''); + } + } + + flashInputError() { + const $input = this.$input; + $input.addClass('crypto-input-error'); + setTimeout(() => $input.removeClass('crypto-input-error'), 600); + $input.val(''); + } + + updatePadlockIcon() { + const note = api.getActiveContextNote(); + if (!note) return; + + const isLocked = note.hasLabel && note.hasLabel('CryptoRoM'); + this.$button.removeClass('bx-lock-alt bx-lock-open-alt bx-loader-alt bx-spin') + .addClass(isLocked ? 'bx-lock-alt' : 'bx-lock-open-alt') + .attr('title', isLocked ? 'CryptoRoM: Locked' : 'CryptoRoM: Unlocked'); + + const $noteDetail = $('.note-detail'); + if (isLocked) { + $noteDetail.css('display', 'none'); + $('.note-detail.crypto-placeholder').remove(); + const $placeholder = $(` +
+ Encrypted Content
+ This note is encrypted. Unlock to view content. +
+ `); + $noteDetail.before($placeholder); + } else { + $noteDetail.css('display', ''); + $('.note-detail.crypto-placeholder').remove(); + } + } + + injectCss() { + if ($('#' + this.styleId).length) return; + $('