generated from DNDs/dnd-template
765 lines
126 KiB
JavaScript
765 lines
126 KiB
JavaScript
|
'use strict';
|
|||
|
|
|||
|
var obsidian = require('obsidian');
|
|||
|
|
|||
|
/*! *****************************************************************************
|
|||
|
Copyright (c) Microsoft Corporation.
|
|||
|
|
|||
|
Permission to use, copy, modify, and/or distribute this software for any
|
|||
|
purpose with or without fee is hereby granted.
|
|||
|
|
|||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|||
|
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|||
|
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|||
|
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|||
|
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|||
|
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|||
|
PERFORMANCE OF THIS SOFTWARE.
|
|||
|
***************************************************************************** */
|
|||
|
|
|||
|
function __awaiter(thisArg, _arguments, P, generator) {
|
|||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
class DecryptModal extends obsidian.Modal {
|
|||
|
constructor(app, title, text = '', showButton) {
|
|||
|
super(app);
|
|||
|
this.decryptInPlace = false;
|
|||
|
this.text = text;
|
|||
|
this.titleEl.innerText = title;
|
|||
|
this.showButton = showButton;
|
|||
|
}
|
|||
|
onOpen() {
|
|||
|
let { contentEl } = this;
|
|||
|
const textEl = contentEl.createDiv().createEl('textarea', { text: this.text });
|
|||
|
textEl.style.width = '100%';
|
|||
|
textEl.style.height = '100%';
|
|||
|
textEl.rows = 10;
|
|||
|
textEl.readOnly = true;
|
|||
|
//textEl.focus(); // Doesn't seem to work here...
|
|||
|
setTimeout(() => { textEl.focus(); }, 100); //... but this does
|
|||
|
const btnContainerEl = contentEl.createDiv('');
|
|||
|
if (this.showButton) {
|
|||
|
const copyBtnEl = btnContainerEl.createEl('button', { text: 'Copy' });
|
|||
|
copyBtnEl.addEventListener('click', () => {
|
|||
|
navigator.clipboard.writeText(textEl.value);
|
|||
|
});
|
|||
|
}
|
|||
|
const decryptInPlaceBtnEl = btnContainerEl.createEl('button', { text: 'Decrypt in-place' });
|
|||
|
decryptInPlaceBtnEl.addEventListener('click', () => {
|
|||
|
this.decryptInPlace = true;
|
|||
|
this.close();
|
|||
|
});
|
|||
|
const cancelBtnEl = btnContainerEl.createEl('button', { text: 'Close' });
|
|||
|
cancelBtnEl.addEventListener('click', () => {
|
|||
|
this.close();
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
class PasswordModal extends obsidian.Modal {
|
|||
|
constructor(app, isEncrypting, confirmPassword, defaultPassword = null, hint) {
|
|||
|
super(app);
|
|||
|
this.password = null;
|
|||
|
this.hint = null;
|
|||
|
this.defaultPassword = null;
|
|||
|
this.defaultPassword = defaultPassword;
|
|||
|
this.confirmPassword = confirmPassword;
|
|||
|
this.isEncrypting = isEncrypting;
|
|||
|
this.hint = hint;
|
|||
|
}
|
|||
|
onOpen() {
|
|||
|
var _a, _b, _c;
|
|||
|
let { contentEl } = this;
|
|||
|
contentEl.empty();
|
|||
|
contentEl.addClass('meld-e-password');
|
|||
|
if (obsidian.Platform.isMobile) {
|
|||
|
contentEl.addClass('meld-e-platform-mobile');
|
|||
|
}
|
|||
|
else if (obsidian.Platform.isDesktop) {
|
|||
|
contentEl.addClass('meld-e-platform-desktop');
|
|||
|
}
|
|||
|
/* Main password input row */
|
|||
|
const inputPwContainerEl = contentEl.createDiv({ cls: 'meld-e-row' });
|
|||
|
inputPwContainerEl.createSpan({ cls: 'meld-e-icon', text: '🔑' });
|
|||
|
const pwInputEl = inputPwContainerEl.createEl('input', { type: 'password', value: (_a = this.defaultPassword) !== null && _a !== void 0 ? _a : '' });
|
|||
|
pwInputEl.placeholder = 'Enter your password';
|
|||
|
pwInputEl.focus();
|
|||
|
if (obsidian.Platform.isMobile) {
|
|||
|
// Add 'Next' button for mobile
|
|||
|
const inputInputNextBtnEl = inputPwContainerEl.createEl('button', {
|
|||
|
text: '→',
|
|||
|
cls: 'meld-e-button-next'
|
|||
|
});
|
|||
|
inputInputNextBtnEl.addEventListener('click', (ev) => {
|
|||
|
inputPasswordHandler();
|
|||
|
});
|
|||
|
}
|
|||
|
/* End Main password input row */
|
|||
|
/* Confirm password input row */
|
|||
|
const confirmPwShown = this.confirmPassword;
|
|||
|
const confirmPwContainerEl = contentEl.createDiv({ cls: 'meld-e-row' });
|
|||
|
confirmPwContainerEl.createSpan({ cls: 'meld-e-icon', text: '🔑' });
|
|||
|
const pwConfirmInputEl = confirmPwContainerEl.createEl('input', {
|
|||
|
type: 'password',
|
|||
|
value: (_b = this.defaultPassword) !== null && _b !== void 0 ? _b : ''
|
|||
|
});
|
|||
|
pwConfirmInputEl.placeholder = 'Confirm your password';
|
|||
|
const messageEl = contentEl.createDiv({ cls: 'meld-e-message' });
|
|||
|
messageEl.hide();
|
|||
|
if (obsidian.Platform.isMobile) {
|
|||
|
// Add 'Next' button for mobile
|
|||
|
const confirmInputNextBtnEl = confirmPwContainerEl.createEl('button', {
|
|||
|
text: '→',
|
|||
|
cls: 'meld-e-button-next'
|
|||
|
});
|
|||
|
confirmInputNextBtnEl.addEventListener('click', (ev) => {
|
|||
|
confirmPasswordHandler();
|
|||
|
});
|
|||
|
}
|
|||
|
if (!confirmPwShown) {
|
|||
|
confirmPwContainerEl.hide();
|
|||
|
}
|
|||
|
/* End Confirm password input row */
|
|||
|
/* Hint input row */
|
|||
|
const hintInputShown = this.isEncrypting;
|
|||
|
const inputHintContainerEl = contentEl.createDiv({ cls: 'meld-e-row' });
|
|||
|
inputHintContainerEl.createSpan({ cls: 'meld-e-icon', text: '💡' });
|
|||
|
const hintInputEl = inputHintContainerEl.createEl('input', { type: 'text', value: this.hint });
|
|||
|
hintInputEl.placeholder = 'Enter an optional password hint';
|
|||
|
if (obsidian.Platform.isMobile) {
|
|||
|
// Add 'Next' button for mobile
|
|||
|
const hintInputNextBtnEl = inputHintContainerEl.createEl('button', {
|
|||
|
text: '→',
|
|||
|
cls: 'meld-e-button-next'
|
|||
|
});
|
|||
|
hintInputNextBtnEl.addEventListener('click', (ev) => {
|
|||
|
hintPasswordHandler();
|
|||
|
});
|
|||
|
}
|
|||
|
if (!hintInputShown) {
|
|||
|
inputHintContainerEl.hide();
|
|||
|
}
|
|||
|
/* End Hint input row */
|
|||
|
/* Hint text row */
|
|||
|
const spanHintContainerEl = contentEl.createDiv({ cls: 'meld-e-row' });
|
|||
|
spanHintContainerEl.createSpan({ cls: 'meld-e-icon', text: '💡' });
|
|||
|
spanHintContainerEl.createSpan({ cls: 'meld-e-hint', text: `Hint: '${this.hint}'` });
|
|||
|
if (hintInputShown || ((_c = this.hint) !== null && _c !== void 0 ? _c : '').length == 0) {
|
|||
|
spanHintContainerEl.hide();
|
|||
|
}
|
|||
|
/* END Hint text row */
|
|||
|
const confirmPwButtonEl = contentEl.createEl('button', {
|
|||
|
text: 'Confirm',
|
|||
|
cls: 'meld-e-button-confirm'
|
|||
|
});
|
|||
|
confirmPwButtonEl.addEventListener('click', (ev) => {
|
|||
|
if (validate()) {
|
|||
|
this.close();
|
|||
|
}
|
|||
|
else {
|
|||
|
pwInputEl.focus();
|
|||
|
}
|
|||
|
});
|
|||
|
const validate = () => {
|
|||
|
if (confirmPwShown) {
|
|||
|
if (pwInputEl.value != pwConfirmInputEl.value) {
|
|||
|
// passwords don't match
|
|||
|
messageEl.setText('Passwords don\'t match');
|
|||
|
messageEl.show();
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
this.password = pwInputEl.value;
|
|||
|
this.hint = hintInputEl.value;
|
|||
|
return true;
|
|||
|
};
|
|||
|
const inputPasswordHandler = () => {
|
|||
|
if (confirmPwShown) {
|
|||
|
pwConfirmInputEl.focus();
|
|||
|
return;
|
|||
|
}
|
|||
|
if (hintInputShown) {
|
|||
|
hintInputEl.focus();
|
|||
|
return;
|
|||
|
}
|
|||
|
if (validate()) {
|
|||
|
this.close();
|
|||
|
}
|
|||
|
};
|
|||
|
const confirmPasswordHandler = () => {
|
|||
|
if (validate()) {
|
|||
|
if (hintInputShown) {
|
|||
|
hintInputEl.focus();
|
|||
|
}
|
|||
|
else {
|
|||
|
this.close();
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
const hintPasswordHandler = () => {
|
|||
|
if (validate()) {
|
|||
|
this.close();
|
|||
|
}
|
|||
|
else {
|
|||
|
pwInputEl.focus();
|
|||
|
}
|
|||
|
};
|
|||
|
hintInputEl.addEventListener('keypress', (ev) => {
|
|||
|
if ((ev.code === 'Enter' || ev.code === 'NumpadEnter')
|
|||
|
&& pwInputEl.value.length > 0) {
|
|||
|
ev.preventDefault();
|
|||
|
hintPasswordHandler();
|
|||
|
}
|
|||
|
});
|
|||
|
pwConfirmInputEl.addEventListener('keypress', (ev) => {
|
|||
|
if ((ev.code === 'Enter' || ev.code === 'NumpadEnter')
|
|||
|
&& pwConfirmInputEl.value.length > 0) {
|
|||
|
ev.preventDefault();
|
|||
|
confirmPasswordHandler();
|
|||
|
}
|
|||
|
});
|
|||
|
pwInputEl.addEventListener('keypress', (ev) => {
|
|||
|
if ((ev.code === 'Enter' || ev.code === 'NumpadEnter')
|
|||
|
&& pwInputEl.value.length > 0) {
|
|||
|
ev.preventDefault();
|
|||
|
inputPasswordHandler();
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const vectorSize = 16;
|
|||
|
const utf8Encoder = new TextEncoder();
|
|||
|
const utf8Decoder = new TextDecoder();
|
|||
|
const iterations = 1000;
|
|||
|
const salt = utf8Encoder.encode('XHWnDAT6ehMVY2zD');
|
|||
|
class CryptoHelperV2 {
|
|||
|
deriveKey(password) {
|
|||
|
return __awaiter(this, void 0, void 0, function* () {
|
|||
|
const buffer = utf8Encoder.encode(password);
|
|||
|
const key = yield crypto.subtle.importKey('raw', buffer, { name: 'PBKDF2' }, false, ['deriveKey']);
|
|||
|
const privateKey = crypto.subtle.deriveKey({
|
|||
|
name: 'PBKDF2',
|
|||
|
hash: { name: 'SHA-256' },
|
|||
|
iterations,
|
|||
|
salt
|
|||
|
}, key, {
|
|||
|
name: 'AES-GCM',
|
|||
|
length: 256
|
|||
|
}, false, ['encrypt', 'decrypt']);
|
|||
|
return privateKey;
|
|||
|
});
|
|||
|
}
|
|||
|
encryptToBase64(text, password) {
|
|||
|
return __awaiter(this, void 0, void 0, function* () {
|
|||
|
const key = yield this.deriveKey(password);
|
|||
|
const textBytesToEncrypt = utf8Encoder.encode(text);
|
|||
|
const vector = crypto.getRandomValues(new Uint8Array(vectorSize));
|
|||
|
// encrypt into bytes
|
|||
|
const encryptedBytes = new Uint8Array(yield crypto.subtle.encrypt({ name: 'AES-GCM', iv: vector }, key, textBytesToEncrypt));
|
|||
|
const finalBytes = new Uint8Array(vector.byteLength + encryptedBytes.byteLength);
|
|||
|
finalBytes.set(vector, 0);
|
|||
|
finalBytes.set(encryptedBytes, vector.byteLength);
|
|||
|
//convert array to base64
|
|||
|
const base64Text = btoa(String.fromCharCode(...finalBytes));
|
|||
|
return base64Text;
|
|||
|
});
|
|||
|
}
|
|||
|
stringToArray(str) {
|
|||
|
var result = [];
|
|||
|
for (var i = 0; i < str.length; i++) {
|
|||
|
result.push(str.charCodeAt(i));
|
|||
|
}
|
|||
|
return new Uint8Array(result);
|
|||
|
}
|
|||
|
decryptFromBase64(base64Encoded, password) {
|
|||
|
return __awaiter(this, void 0, void 0, function* () {
|
|||
|
try {
|
|||
|
let bytesToDecode = this.stringToArray(atob(base64Encoded));
|
|||
|
// extract iv
|
|||
|
const vector = bytesToDecode.slice(0, vectorSize);
|
|||
|
// extract encrypted text
|
|||
|
const encryptedTextBytes = bytesToDecode.slice(vectorSize);
|
|||
|
const key = yield this.deriveKey(password);
|
|||
|
// decrypt into bytes
|
|||
|
let decryptedBytes = yield crypto.subtle.decrypt({ name: 'AES-GCM', iv: vector }, key, encryptedTextBytes);
|
|||
|
// convert bytes to text
|
|||
|
let decryptedText = utf8Decoder.decode(decryptedBytes);
|
|||
|
return decryptedText;
|
|||
|
}
|
|||
|
catch (e) {
|
|||
|
//console.error(e);
|
|||
|
return null;
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
const algorithmObsolete = {
|
|||
|
name: 'AES-GCM',
|
|||
|
iv: new Uint8Array([196, 190, 240, 190, 188, 78, 41, 132, 15, 220, 84, 211]),
|
|||
|
tagLength: 128
|
|||
|
};
|
|||
|
class CryptoHelperObsolete {
|
|||
|
buildKey(password) {
|
|||
|
return __awaiter(this, void 0, void 0, function* () {
|
|||
|
let utf8Encode = new TextEncoder();
|
|||
|
let passwordBytes = utf8Encode.encode(password);
|
|||
|
let passwordDigest = yield crypto.subtle.digest({ name: 'SHA-256' }, passwordBytes);
|
|||
|
let key = yield crypto.subtle.importKey('raw', passwordDigest, algorithmObsolete, false, ['encrypt', 'decrypt']);
|
|||
|
return key;
|
|||
|
});
|
|||
|
}
|
|||
|
encryptToBase64(text, password) {
|
|||
|
return __awaiter(this, void 0, void 0, function* () {
|
|||
|
let key = yield this.buildKey(password);
|
|||
|
let utf8Encode = new TextEncoder();
|
|||
|
let bytesToEncrypt = utf8Encode.encode(text);
|
|||
|
// encrypt into bytes
|
|||
|
let encryptedBytes = new Uint8Array(yield crypto.subtle.encrypt(algorithmObsolete, key, bytesToEncrypt));
|
|||
|
//convert array to base64
|
|||
|
let base64Text = btoa(String.fromCharCode(...encryptedBytes));
|
|||
|
return base64Text;
|
|||
|
});
|
|||
|
}
|
|||
|
stringToArray(str) {
|
|||
|
var result = [];
|
|||
|
for (var i = 0; i < str.length; i++) {
|
|||
|
result.push(str.charCodeAt(i));
|
|||
|
}
|
|||
|
return new Uint8Array(result);
|
|||
|
}
|
|||
|
decryptFromBase64(base64Encoded, password) {
|
|||
|
return __awaiter(this, void 0, void 0, function* () {
|
|||
|
try {
|
|||
|
// convert base 64 to array
|
|||
|
let bytesToDecrypt = this.stringToArray(atob(base64Encoded));
|
|||
|
let key = yield this.buildKey(password);
|
|||
|
// decrypt into bytes
|
|||
|
let decryptedBytes = yield crypto.subtle.decrypt(algorithmObsolete, key, bytesToDecrypt);
|
|||
|
// convert bytes to text
|
|||
|
let utf8Decode = new TextDecoder();
|
|||
|
let decryptedText = utf8Decode.decode(decryptedBytes);
|
|||
|
return decryptedText;
|
|||
|
}
|
|||
|
catch (e) {
|
|||
|
return null;
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
class MeldEncryptSettingsTab extends obsidian.PluginSettingTab {
|
|||
|
constructor(app, plugin) {
|
|||
|
super(app, plugin);
|
|||
|
this.plugin = plugin;
|
|||
|
}
|
|||
|
display() {
|
|||
|
let { containerEl } = this;
|
|||
|
containerEl.empty();
|
|||
|
containerEl.createEl('h2', { text: 'Settings for Meld Encrypt' });
|
|||
|
new obsidian.Setting(containerEl)
|
|||
|
.setName('Expand selection to whole line?')
|
|||
|
.setDesc('Partial selections will get expanded to the whole line.')
|
|||
|
.addToggle(toggle => {
|
|||
|
toggle
|
|||
|
.setValue(this.plugin.settings.expandToWholeLines)
|
|||
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
|||
|
this.plugin.settings.expandToWholeLines = value;
|
|||
|
yield this.plugin.saveSettings();
|
|||
|
//this.updateSettingsUi();
|
|||
|
}));
|
|||
|
});
|
|||
|
new obsidian.Setting(containerEl)
|
|||
|
.setName('Confirm password?')
|
|||
|
.setDesc('Confirm password when encrypting.')
|
|||
|
.addToggle(toggle => {
|
|||
|
toggle
|
|||
|
.setValue(this.plugin.settings.confirmPassword)
|
|||
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
|||
|
this.plugin.settings.confirmPassword = value;
|
|||
|
yield this.plugin.saveSettings();
|
|||
|
this.updateSettingsUi();
|
|||
|
}));
|
|||
|
});
|
|||
|
new obsidian.Setting(containerEl)
|
|||
|
.setName('Copy button?')
|
|||
|
.setDesc('Show a button to copy decrypted text.')
|
|||
|
.addToggle(toggle => {
|
|||
|
toggle
|
|||
|
.setValue(this.plugin.settings.showButton)
|
|||
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
|||
|
this.plugin.settings.showButton = value;
|
|||
|
yield this.plugin.saveSettings();
|
|||
|
this.updateSettingsUi();
|
|||
|
}));
|
|||
|
});
|
|||
|
new obsidian.Setting(containerEl)
|
|||
|
.setName('Remember password?')
|
|||
|
.setDesc('Remember the last used password for this session.')
|
|||
|
.addToggle(toggle => {
|
|||
|
toggle
|
|||
|
.setValue(this.plugin.settings.rememberPassword)
|
|||
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
|||
|
this.plugin.settings.rememberPassword = value;
|
|||
|
yield this.plugin.saveSettings();
|
|||
|
this.updateSettingsUi();
|
|||
|
}));
|
|||
|
});
|
|||
|
this.pwTimeoutSetting = new obsidian.Setting(containerEl)
|
|||
|
.setName(this.buildPasswordTimeoutSettingName())
|
|||
|
.setDesc('The number of minutes to remember the last used password.')
|
|||
|
.addSlider(slider => {
|
|||
|
slider
|
|||
|
.setLimits(0, 120, 5)
|
|||
|
.setValue(this.plugin.settings.rememberPasswordTimeout)
|
|||
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
|||
|
this.plugin.settings.rememberPasswordTimeout = value;
|
|||
|
yield this.plugin.saveSettings();
|
|||
|
this.updateSettingsUi();
|
|||
|
}));
|
|||
|
});
|
|||
|
this.updateSettingsUi();
|
|||
|
}
|
|||
|
updateSettingsUi() {
|
|||
|
this.pwTimeoutSetting.setName(this.buildPasswordTimeoutSettingName());
|
|||
|
if (this.plugin.settings.rememberPassword) {
|
|||
|
this.pwTimeoutSetting.settingEl.show();
|
|||
|
}
|
|||
|
else {
|
|||
|
this.pwTimeoutSetting.settingEl.hide();
|
|||
|
}
|
|||
|
}
|
|||
|
buildPasswordTimeoutSettingName() {
|
|||
|
const value = this.plugin.settings.rememberPasswordTimeout;
|
|||
|
let timeoutString = `${value} minutes`;
|
|||
|
if (value == 0) {
|
|||
|
timeoutString = 'Never forget';
|
|||
|
}
|
|||
|
return `Remember Password Timeout (${timeoutString})`;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const _PREFIX = '%%🔐';
|
|||
|
const _PREFIX_OBSOLETE = _PREFIX + ' ';
|
|||
|
const _PREFIX_A = _PREFIX + 'α ';
|
|||
|
const _SUFFIX = ' 🔐%%';
|
|||
|
const _HINT = '💡';
|
|||
|
const DEFAULT_SETTINGS = {
|
|||
|
expandToWholeLines: true,
|
|||
|
confirmPassword: true,
|
|||
|
showButton: false,
|
|||
|
rememberPassword: true,
|
|||
|
rememberPasswordTimeout: 30
|
|||
|
};
|
|||
|
class MeldEncrypt extends obsidian.Plugin {
|
|||
|
onload() {
|
|||
|
return __awaiter(this, void 0, void 0, function* () {
|
|||
|
yield this.loadSettings();
|
|||
|
this.addSettingTab(new MeldEncryptSettingsTab(this.app, this));
|
|||
|
this.addCommand({
|
|||
|
id: 'meld-encrypt',
|
|||
|
name: 'Encrypt/Decrypt',
|
|||
|
editorCheckCallback: (checking, editor, view) => this.processEncryptDecryptCommand(checking, editor, view, false)
|
|||
|
});
|
|||
|
this.addCommand({
|
|||
|
id: 'meld-encrypt-in-place',
|
|||
|
name: 'Encrypt/Decrypt In-place',
|
|||
|
editorCheckCallback: (checking, editor, view) => this.processEncryptDecryptCommand(checking, editor, view, true)
|
|||
|
});
|
|||
|
this.addCommand({
|
|||
|
id: 'meld-encrypt-note',
|
|||
|
name: 'Encrypt/Decrypt Whole Note',
|
|||
|
editorCheckCallback: (checking, editor, view) => this.processEncryptDecryptWholeNoteCommand(checking, editor, view)
|
|||
|
});
|
|||
|
});
|
|||
|
}
|
|||
|
loadSettings() {
|
|||
|
return __awaiter(this, void 0, void 0, function* () {
|
|||
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, yield this.loadData());
|
|||
|
});
|
|||
|
}
|
|||
|
saveSettings() {
|
|||
|
return __awaiter(this, void 0, void 0, function* () {
|
|||
|
yield this.saveData(this.settings);
|
|||
|
});
|
|||
|
}
|
|||
|
isSettingsModalOpen() {
|
|||
|
return document.querySelector('.mod-settings') !== null;
|
|||
|
}
|
|||
|
processEncryptDecryptWholeNoteCommand(checking, editor, view) {
|
|||
|
if (checking && this.isSettingsModalOpen()) {
|
|||
|
// Settings is open, ensures this command can show up in other
|
|||
|
// plugins which list commands e.g. customizable-sidebar
|
|||
|
return true;
|
|||
|
}
|
|||
|
const startPos = editor.offsetToPos(0);
|
|||
|
const endPos = { line: editor.lastLine(), ch: editor.getLine(editor.lastLine()).length };
|
|||
|
const selectionText = editor.getRange(startPos, endPos).trim();
|
|||
|
return this.processSelection(checking, editor, selectionText, startPos, endPos, true);
|
|||
|
}
|
|||
|
processEncryptDecryptCommand(checking, editor, view, decryptInPlace) {
|
|||
|
if (checking && this.isSettingsModalOpen()) {
|
|||
|
// Settings is open, ensures this command can show up in other
|
|||
|
// plugins which list commands e.g. customizable-sidebar
|
|||
|
return true;
|
|||
|
}
|
|||
|
let startPos = editor.getCursor('from');
|
|||
|
let endPos = editor.getCursor('to');
|
|||
|
if (this.settings.expandToWholeLines) {
|
|||
|
const startLine = startPos.line;
|
|||
|
startPos = { line: startLine, ch: 0 }; // want the start of the first line
|
|||
|
const endLine = endPos.line;
|
|||
|
const endLineText = editor.getLine(endLine);
|
|||
|
endPos = { line: endLine, ch: endLineText.length }; // want the end of last line
|
|||
|
}
|
|||
|
else {
|
|||
|
if (!editor.somethingSelected()) {
|
|||
|
// nothing selected, assume user wants to decrypt, expand to start and end markers
|
|||
|
startPos = this.getClosestPrevTextCursorPos(editor, _PREFIX, startPos);
|
|||
|
endPos = this.getClosestNextTextCursorPos(editor, _SUFFIX, endPos);
|
|||
|
}
|
|||
|
}
|
|||
|
const selectionText = editor.getRange(startPos, endPos);
|
|||
|
return this.processSelection(checking, editor, selectionText, startPos, endPos, decryptInPlace);
|
|||
|
}
|
|||
|
getClosestPrevTextCursorPos(editor, text, defaultValue) {
|
|||
|
const initOffset = editor.posToOffset(editor.getCursor("from"));
|
|||
|
for (let offset = initOffset; offset >= 0; offset--) {
|
|||
|
const offsetPos = editor.offsetToPos(offset);
|
|||
|
const textEndOffset = offset + text.length;
|
|||
|
const prefixEndPos = editor.offsetToPos(textEndOffset);
|
|||
|
const testText = editor.getRange(offsetPos, prefixEndPos);
|
|||
|
if (testText == text) {
|
|||
|
return offsetPos;
|
|||
|
}
|
|||
|
}
|
|||
|
return defaultValue;
|
|||
|
}
|
|||
|
getClosestNextTextCursorPos(editor, text, defaultValue) {
|
|||
|
const initOffset = editor.posToOffset(editor.getCursor("from"));
|
|||
|
const lastLineNum = editor.lastLine();
|
|||
|
let maxOffset = editor.posToOffset({ line: lastLineNum, ch: editor.getLine(lastLineNum).length });
|
|||
|
for (let offset = initOffset; offset <= maxOffset - text.length; offset++) {
|
|||
|
const offsetPos = editor.offsetToPos(offset);
|
|||
|
const textEndOffset = offset + text.length;
|
|||
|
const prefixEndPos = editor.offsetToPos(textEndOffset);
|
|||
|
const testText = editor.getRange(offsetPos, prefixEndPos);
|
|||
|
if (testText == text) {
|
|||
|
return prefixEndPos;
|
|||
|
}
|
|||
|
}
|
|||
|
return defaultValue;
|
|||
|
}
|
|||
|
analyseSelection(selectionText) {
|
|||
|
const result = new SelectionAnalysis();
|
|||
|
result.isEmpty = selectionText.length === 0;
|
|||
|
result.hasObsoleteEncryptedPrefix = selectionText.startsWith(_PREFIX_OBSOLETE);
|
|||
|
result.hasEncryptedPrefix = result.hasObsoleteEncryptedPrefix || selectionText.startsWith(_PREFIX_A);
|
|||
|
result.hasDecryptSuffix = selectionText.endsWith(_SUFFIX);
|
|||
|
result.containsEncryptedMarkers =
|
|||
|
selectionText.contains(_PREFIX_OBSOLETE)
|
|||
|
|| selectionText.contains(_PREFIX_A)
|
|||
|
|| selectionText.contains(_SUFFIX);
|
|||
|
result.canDecrypt = result.hasEncryptedPrefix && result.hasDecryptSuffix;
|
|||
|
result.canEncrypt = !result.hasEncryptedPrefix && !result.containsEncryptedMarkers;
|
|||
|
if (result.canDecrypt) {
|
|||
|
result.decryptable = this.parseDecryptableContent(selectionText);
|
|||
|
if (result.decryptable == null) {
|
|||
|
result.canDecrypt = false;
|
|||
|
}
|
|||
|
}
|
|||
|
return result;
|
|||
|
}
|
|||
|
processSelection(checking, editor, selectionText, finalSelectionStart, finalSelectionEnd, decryptInPlace) {
|
|||
|
var _a;
|
|||
|
const selectionAnalysis = this.analyseSelection(selectionText);
|
|||
|
if (selectionAnalysis.isEmpty) {
|
|||
|
if (!checking) {
|
|||
|
new obsidian.Notice('Nothing to Encrypt.');
|
|||
|
}
|
|||
|
return false;
|
|||
|
}
|
|||
|
if (!selectionAnalysis.canDecrypt && !selectionAnalysis.canEncrypt) {
|
|||
|
if (!checking) {
|
|||
|
new obsidian.Notice('Unable to Encrypt or Decrypt that.');
|
|||
|
}
|
|||
|
return false;
|
|||
|
}
|
|||
|
if (checking) {
|
|||
|
return true;
|
|||
|
}
|
|||
|
// Fetch password from user
|
|||
|
// determine default password
|
|||
|
const isRememberPasswordExpired = !this.settings.rememberPassword
|
|||
|
|| (this.passwordLastUsedExpiry != null
|
|||
|
&& Date.now() > this.passwordLastUsedExpiry);
|
|||
|
const confirmPassword = selectionAnalysis.canEncrypt && this.settings.confirmPassword;
|
|||
|
if (isRememberPasswordExpired || confirmPassword) {
|
|||
|
// forget password
|
|||
|
this.passwordLastUsed = '';
|
|||
|
}
|
|||
|
const pwModal = new PasswordModal(this.app, selectionAnalysis.canEncrypt, confirmPassword, this.passwordLastUsed, (_a = selectionAnalysis.decryptable) === null || _a === void 0 ? void 0 : _a.hint);
|
|||
|
pwModal.onClose = () => {
|
|||
|
var _a;
|
|||
|
const pw = (_a = pwModal.password) !== null && _a !== void 0 ? _a : '';
|
|||
|
if (pw.length == 0) {
|
|||
|
return;
|
|||
|
}
|
|||
|
const hint = pwModal.hint;
|
|||
|
// remember password?
|
|||
|
if (this.settings.rememberPassword) {
|
|||
|
this.passwordLastUsed = pw;
|
|||
|
this.passwordLastUsedExpiry =
|
|||
|
this.settings.rememberPasswordTimeout == 0
|
|||
|
? null
|
|||
|
: Date.now() + this.settings.rememberPasswordTimeout * 1000 * 60 // new expiry
|
|||
|
;
|
|||
|
}
|
|||
|
if (selectionAnalysis.canEncrypt) {
|
|||
|
const encryptable = new Encryptable();
|
|||
|
encryptable.text = selectionText;
|
|||
|
encryptable.hint = hint;
|
|||
|
this.encryptSelection(editor, encryptable, pw, finalSelectionStart, finalSelectionEnd);
|
|||
|
}
|
|||
|
else {
|
|||
|
if (selectionAnalysis.decryptable.version == 1) {
|
|||
|
this.decryptSelection_a(editor, selectionAnalysis.decryptable, pw, finalSelectionStart, finalSelectionEnd, decryptInPlace);
|
|||
|
}
|
|||
|
else {
|
|||
|
this.decryptSelectionObsolete(editor, selectionAnalysis.decryptable, pw, finalSelectionStart, finalSelectionEnd, decryptInPlace);
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
pwModal.open();
|
|||
|
return true;
|
|||
|
}
|
|||
|
encryptSelection(editor, encryptable, password, finalSelectionStart, finalSelectionEnd) {
|
|||
|
return __awaiter(this, void 0, void 0, function* () {
|
|||
|
//encrypt
|
|||
|
const crypto = new CryptoHelperV2();
|
|||
|
const encodedText = this.encodeEncryption(yield crypto.encryptToBase64(encryptable.text, password), encryptable.hint);
|
|||
|
editor.setSelection(finalSelectionStart, finalSelectionEnd);
|
|||
|
editor.replaceSelection(encodedText);
|
|||
|
});
|
|||
|
}
|
|||
|
decryptSelection_a(editor, decryptable, password, selectionStart, selectionEnd, decryptInPlace) {
|
|||
|
return __awaiter(this, void 0, void 0, function* () {
|
|||
|
// decrypt
|
|||
|
const crypto = new CryptoHelperV2();
|
|||
|
const decryptedText = yield crypto.decryptFromBase64(decryptable.base64CipherText, password);
|
|||
|
if (decryptedText === null) {
|
|||
|
new obsidian.Notice('❌ Decryption failed!');
|
|||
|
}
|
|||
|
else {
|
|||
|
if (decryptInPlace) {
|
|||
|
editor.setSelection(selectionStart, selectionEnd);
|
|||
|
editor.replaceSelection(decryptedText);
|
|||
|
}
|
|||
|
else {
|
|||
|
const decryptModal = new DecryptModal(this.app, '🔓', decryptedText, this.settings.showButton);
|
|||
|
decryptModal.onClose = () => {
|
|||
|
editor.focus();
|
|||
|
if (decryptModal.decryptInPlace) {
|
|||
|
editor.setSelection(selectionStart, selectionEnd);
|
|||
|
editor.replaceSelection(decryptedText);
|
|||
|
}
|
|||
|
};
|
|||
|
decryptModal.open();
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
decryptSelectionObsolete(editor, decryptable, password, selectionStart, selectionEnd, decryptInPlace) {
|
|||
|
return __awaiter(this, void 0, void 0, function* () {
|
|||
|
// decrypt
|
|||
|
const base64CipherText = this.removeMarkers(decryptable.base64CipherText);
|
|||
|
const crypto = new CryptoHelperObsolete();
|
|||
|
const decryptedText = yield crypto.decryptFromBase64(base64CipherText, password);
|
|||
|
if (decryptedText === null) {
|
|||
|
new obsidian.Notice('❌ Decryption failed!');
|
|||
|
}
|
|||
|
else {
|
|||
|
if (decryptInPlace) {
|
|||
|
editor.setSelection(selectionStart, selectionEnd);
|
|||
|
editor.replaceSelection(decryptedText);
|
|||
|
}
|
|||
|
else {
|
|||
|
const decryptModal = new DecryptModal(this.app, '🔓', decryptedText, this.settings.showButton);
|
|||
|
decryptModal.onClose = () => {
|
|||
|
editor.focus();
|
|||
|
if (decryptModal.decryptInPlace) {
|
|||
|
editor.setSelection(selectionStart, selectionEnd);
|
|||
|
editor.replaceSelection(decryptedText);
|
|||
|
}
|
|||
|
};
|
|||
|
decryptModal.open();
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
parseDecryptableContent(text) {
|
|||
|
const result = new Decryptable();
|
|||
|
let content = text;
|
|||
|
if (content.startsWith(_PREFIX_A) && content.endsWith(_SUFFIX)) {
|
|||
|
result.version = 1;
|
|||
|
content = content.replace(_PREFIX_A, '').replace(_SUFFIX, '');
|
|||
|
}
|
|||
|
else if (content.startsWith(_PREFIX_OBSOLETE) && content.endsWith(_SUFFIX)) {
|
|||
|
result.version = 0;
|
|||
|
content = content.replace(_PREFIX_OBSOLETE, '').replace(_SUFFIX, '');
|
|||
|
}
|
|||
|
else {
|
|||
|
return null; // invalid format
|
|||
|
}
|
|||
|
// check if there is a hint
|
|||
|
//console.table(content);
|
|||
|
if (content.substr(0, _HINT.length) == _HINT) {
|
|||
|
const endHintMarker = content.indexOf(_HINT, _HINT.length);
|
|||
|
if (endHintMarker < 0) {
|
|||
|
return null; // invalid format
|
|||
|
}
|
|||
|
result.hint = content.substring(_HINT.length, endHintMarker);
|
|||
|
result.base64CipherText = content.substring(endHintMarker + _HINT.length);
|
|||
|
}
|
|||
|
else {
|
|||
|
result.base64CipherText = content;
|
|||
|
}
|
|||
|
//console.table(result);
|
|||
|
return result;
|
|||
|
}
|
|||
|
removeMarkers(text) {
|
|||
|
if (text.startsWith(_PREFIX_A) && text.endsWith(_SUFFIX)) {
|
|||
|
return text.replace(_PREFIX_A, '').replace(_SUFFIX, '');
|
|||
|
}
|
|||
|
if (text.startsWith(_PREFIX_OBSOLETE) && text.endsWith(_SUFFIX)) {
|
|||
|
return text.replace(_PREFIX_OBSOLETE, '').replace(_SUFFIX, '');
|
|||
|
}
|
|||
|
return text;
|
|||
|
}
|
|||
|
encodeEncryption(encryptedText, hint) {
|
|||
|
if (!encryptedText.contains(_PREFIX_OBSOLETE) && !encryptedText.contains(_PREFIX_A) && !encryptedText.contains(_SUFFIX)) {
|
|||
|
if (hint) {
|
|||
|
return _PREFIX_A.concat(_HINT, hint, _HINT, encryptedText, _SUFFIX);
|
|||
|
}
|
|||
|
return _PREFIX_A.concat(encryptedText, _SUFFIX);
|
|||
|
}
|
|||
|
return encryptedText;
|
|||
|
}
|
|||
|
}
|
|||
|
class SelectionAnalysis {
|
|||
|
}
|
|||
|
class Encryptable {
|
|||
|
}
|
|||
|
class Decryptable {
|
|||
|
}
|
|||
|
|
|||
|
module.exports = MeldEncrypt;
|
|||
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFpbi5qcyIsInNvdXJjZXMiOlsiLi4vbm9kZV9tb2R1bGVzL3RzbGliL3RzbGliLmVzNi5qcyIsIi4uL3NyYy9EZWNyeXB0TW9kYWwudHMiLCIuLi9zcmMvUGFzc3dvcmRNb2RhbC50cyIsIi4uL3NyYy9DcnlwdG9IZWxwZXIudHMiLCIuLi9zcmMvTWVsZEVuY3J5cHRTZXR0aW5nc1RhYi50cyIsIi4uL3NyYy9tYWluLnRzIl0sInNvdXJjZXNDb250ZW50IjpbIi8qISAqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKlxyXG5Db3B5cmlnaHQgKGMpIE1pY3Jvc29mdCBDb3Jwb3JhdGlvbi5cclxuXHJcblBlcm1pc3Npb24gdG8gdXNlLCBjb3B5LCBtb2RpZnksIGFuZC9vciBkaXN0cmlidXRlIHRoaXMgc29mdHdhcmUgZm9yIGFueVxyXG5wdXJwb3NlIHdpdGggb3Igd2l0aG91dCBmZWUgaXMgaGVyZWJ5IGdyYW50ZWQuXHJcblxyXG5USEUgU09GVFdBUkUgSVMgUFJPVklERUQgXCJBUyBJU1wiIEFORCBUSEUgQVVUSE9SIERJU0NMQUlNUyBBTEwgV0FSUkFOVElFUyBXSVRIXHJcblJFR0FSRCBUTyBUSElTIFNPRlRXQVJFIElOQ0xVRElORyBBTEwgSU1QTElFRCBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWVxyXG5BTkQgRklUTkVTUy4gSU4gTk8gRVZFTlQgU0hBTEwgVEhFIEFVVEhPUiBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsXHJcbklORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTVxyXG5MT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUlxyXG5PVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SXHJcblBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuXHJcbioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqICovXHJcbi8qIGdsb2JhbCBSZWZsZWN0LCBQcm9taXNlICovXHJcblxyXG52YXIgZXh0ZW5kU3RhdGljcyA9IGZ1bmN0aW9uKGQsIGIpIHtcclxuICAgIGV4dGVuZFN0YXRpY3MgPSBPYmplY3Quc2V0UHJvdG90eXBlT2YgfHxcclxuICAgICAgICAoeyBfX3Byb3RvX186IFtdIH0gaW5zdGFuY2VvZiBBcnJheSAmJiBmdW5jdGlvbiAoZCwgYikgeyBkLl9fcHJvdG9fXyA9IGI7IH0pIHx8XHJcbiAgICAgICAgZnVuY3Rpb24gKGQsIGIpIHsgZm9yICh2YXIgcCBpbiBiKSBpZiAoT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKGIsIHApKSBkW3BdID0gYltwXTsgfTtcclxuICAgIHJldHVybiBleHRlbmRTdGF0aWNzKGQsIGIpO1xyXG59O1xyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIF9fZXh0ZW5kcyhkLCBiKSB7XHJcbiAgICBpZiAodHlwZW9mIGIgIT09IFwiZnVuY3Rpb25cIiAmJiBiICE9PSBudWxsKVxyXG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoXCJDbGFzcyBleHRlbmRzIHZhbHVlIFwiICsgU3RyaW5nKGIpICsgXCIgaXMgbm90IGEgY29uc3RydWN0b3Igb3IgbnVsbFwiKTtcclxuICAgIGV4dGVuZFN0YXRpY3MoZCwgYik7XHJcbiAgICBmdW5jdGlvbiBfXygpIHsgdGhpcy5jb25zdHJ1Y3RvciA9IGQ7IH1cclxuICAgIGQucHJvdG90eXBlID0gYiA9PT0gbnVsbCA/IE9iamVjdC5jcmVhdGUoYikgOiAoX18ucHJvdG90eXBlID0gYi5wcm90b3R5cGUsIG5ldyBfXygpKTtcclxufVxyXG5cclxuZXhwb3J0IHZhciBfX2Fzc2lnbiA9IGZ1bmN0aW9uKCkge1xyXG4gICAgX19hc3NpZ24gPSBPYmplY3QuYXNzaWduIHx8IGZ1bmN0aW9uIF9fYXNzaWduKHQpIHtcclxuICAgICAgICBmb3IgKHZhciBzLCBpID0gMSwgbiA9IGFyZ3VtZW50cy5sZW5ndGg7IGkgPCBuOyBpKyspIHtcclxuICAgICAgICAgICAgcyA9IGFyZ3VtZW50c1tpXTtcclxuICAgICAgICAgICAgZm9yICh2YXIgcCBpbiBzKSBpZiAoT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKHMsIHApKSB0W3BdID0gc1twXTtcclxuICAgICAgICB9XHJcbiAgICAgICAgcmV0dXJuIHQ7XHJcbiAgICB9XHJcbiAgICByZXR1cm4gX19hc3NpZ24uYXBwbHkodGhpcywgYXJndW1lbnRzKTtcclxufVxyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIF9fcmVzdChzLCBlKSB7XHJcbiAgICB2YXIgdCA9IHt9O1xyXG4gICAgZm9yICh2YXIgcCBpbiBzKSBpZiAoT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKHMsIHApICYmIGUuaW5kZXhPZihwKSA8IDApXHJcbiAgICAgICAgdFtwXSA9IHNbcF07XHJcbiAgICBpZiAocyAhPSBudWxsICYmIHR5cGVvZiBPYmplY3QuZ2V0T3duUHJvcGVydHlTeW1ib2xzID09PSBcImZ1bmN0aW9uXCIpXHJcbiAgICAgICAgZm9yICh2YXIgaSA9IDAsIHAgPSBPYmplY3QuZ2V0T3duUHJvcGVydHlTeW1ib2xzKHMpOyBpIDwgcC5sZW5ndGg7IGkrKykge1xyXG4gICAgICAgICAgICBpZiAoZS5pbmRleE9mKHBbaV0pIDwgMCAmJiBPYmplY3QucHJvdG90eXBlLnByb3BlcnR5SXNFbnVtZXJhYmxlLmNhbGwocywgcFtpXSkpXHJcbiAgICAgICAgICAgICAgICB0W3BbaV1dID0gc1twW2ldXTtcclxuICAgICAgICB9XHJcbiAgICByZXR1cm4gdDtcclxufVxyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIF9fZGVjb3JhdGUoZGVjb3JhdG9ycywgdGFyZ2V0LCBrZXksIGRlc2MpIHtcclxuICAgIHZhciBjID0gYXJndW1lbnRzLmxlbmd0aCwgciA9IGMgPCAzID8gdGFyZ2V0IDogZGVzYyA9PT0gbnVsbCA/IGRlc2MgPSBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yKHRhcmdldCwga2V5KSA6IGRlc2MsIGQ7XHJcbiAgICBpZiAodHlwZW9mIFJlZmxlY3QgPT09IFwib2JqZWN0XCIgJiYgdHlwZW9mIFJlZmxlY3QuZGVjb3JhdGUgPT09IFwiZnVuY3Rpb25cIikgciA9IFJlZmxlY3QuZGVjb3JhdGUoZGVjb3JhdG9ycyw
|