(() => {
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 = $('<div class="padlock-action"></div>');
// Container for countdown + input to keep them aligned
this.$inputContainer = $('<div style="display:flex; align-items:center; left: -275px; position: absolute;"></div>');
this.$widget.append(this.$inputContainer);
// Countdown display (left of input)
this.$countdown = $('<span class="crypto-countdown" style="margin-right:5px; cursor:pointer; width:120px;">0:00</span>');
this.$inputContainer.append(this.$countdown);
this.$countdown.on('click', () => this.addTimeToActiveNote(300)); // add 5 minutes on click
// Password input
this.$input = $('<input type="password" class="crypto-input" placeholder="password" />').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 = $('<button type="button" class="icon-action bx bx-lock-open-alt" title="CryptoRoM: Unlocked"></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 = $(`
<div class="note-detail component crypto-placeholder" style="padding:10px;margin-bottom:10px;text-align:center">
<strong>Encrypted Content</strong><br>
This note is encrypted. Unlock to view content.
</div>
`);
$noteDetail.before($placeholder);
} else {
$noteDetail.css('display', '');
$('.note-detail.crypto-placeholder').remove();
}
}
injectCss() {
if ($('#' + this.styleId).length) return;
$('<style>').attr('id', this.styleId).html(`
.padlock-action { display:inline-flex; align-items:center; margin:0; padding:0; margin-right:20px; }
.padlock-action .crypto-input { display:inline-block; width:120px; margin-right:5px; vertical-align:middle; padding:3px 6px; font-size:14px; border:1px solid #ccc; border-radius:4px; box-sizing:border-box; }
.padlock-action .bx-lock-alt, .padlock-action .bx-lock-open-alt, .padlock-action .bx-loader-alt { font-size:18px; opacity:0.8; cursor:pointer; transition:opacity 0.2s ease; }
.padlock-action .bx-lock-alt:hover, .padlock-action .bx-lock-open-alt:hover { opacity:1; }
.bx-spin { animation:bx-spin 1s linear infinite; }
@keyframes bx-spin { 100% { transform:rotate(360deg); } }
.padlock-action .bx-lock-alt { color: rgb(102,204,102); }
.padlock-action .bx-lock-open-alt { color: var(--main-text-color); }
.padlock-action .bx-loader-alt { color: var(--accent-color,#999); }
.crypto-input-error { border-color:red !important; animation:flash-border 0.6s ease-in-out; }
@keyframes flash-border { 0%,100%{border-color:red;} 50%{border-color:#ccc;} }
.crypto-countdown { font-size:12px; color:bbb; min-width:35px; text-align:right; display:inline-block; cursor:pointer; }
.icon-action:hover{ border: none; }
.padlock-action button {
outline: none;
border: none;
padding: 0;
margin: 0;
}
`).appendTo('head');
}
onunload() {
Object.keys(this.noteTimers).forEach(id => clearInterval(this.noteTimers[id]));
if (this.$widget) this.$widget.remove();
$('#' + this.styleId).remove();
}
}
module.exports = new PadlockWidget();
})();
async function hashPassword(password) {
const data = new TextEncoder().encode(password);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
return Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}