|
|
|
|
'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 MemoryCache {
|
|
|
|
|
constructor() {
|
|
|
|
|
this.values = new Map();
|
|
|
|
|
}
|
|
|
|
|
put(key, value) {
|
|
|
|
|
//console.debug('MemoryCache.put', {key, value});
|
|
|
|
|
this.values.set(key, value);
|
|
|
|
|
}
|
|
|
|
|
get(key, defaultValue) {
|
|
|
|
|
//console.debug('MemoryCache.get', {key, defaultValue});
|
|
|
|
|
return this.values.has(key) ? this.values.get(key) : defaultValue;
|
|
|
|
|
}
|
|
|
|
|
getFirst(keys, defaultValue) {
|
|
|
|
|
//console.debug('MemoryCache.getFirst', {keys, defaultValue});
|
|
|
|
|
for (let index = 0; index < keys.length; index++) {
|
|
|
|
|
const key = keys[index];
|
|
|
|
|
if (this.containsKey(key)) {
|
|
|
|
|
return this.get(key, defaultValue);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return defaultValue;
|
|
|
|
|
}
|
|
|
|
|
containsKey(key) {
|
|
|
|
|
//console.debug('MemoryCache.containsKey', {key});
|
|
|
|
|
return this.values.has(key);
|
|
|
|
|
}
|
|
|
|
|
getKeys() {
|
|
|
|
|
//console.debug('MemoryCache.getKeys');
|
|
|
|
|
return Array.from(this.values.keys());
|
|
|
|
|
}
|
|
|
|
|
clear() {
|
|
|
|
|
//console.debug('MemoryCache.clear');
|
|
|
|
|
this.values.clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class SessionPasswordService {
|
|
|
|
|
static setActive(isActive) {
|
|
|
|
|
SessionPasswordService.isActive = isActive;
|
|
|
|
|
if (!SessionPasswordService.isActive) {
|
|
|
|
|
this.clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param minutesToExpire set to 0 to never expire
|
|
|
|
|
*/
|
|
|
|
|
static setAutoExpire(minutesToExpire) {
|
|
|
|
|
SessionPasswordService.baseMinutesToExpire = minutesToExpire;
|
|
|
|
|
SessionPasswordService.updateExpiryTime();
|
|
|
|
|
}
|
|
|
|
|
static updateExpiryTime() {
|
|
|
|
|
if (SessionPasswordService.baseMinutesToExpire == 0
|
|
|
|
|
|| SessionPasswordService.baseMinutesToExpire == null) {
|
|
|
|
|
SessionPasswordService.expiryTime = null;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
SessionPasswordService.expiryTime = Date.now() + SessionPasswordService.baseMinutesToExpire * 1000 * 60;
|
|
|
|
|
}
|
|
|
|
|
console.debug('SessionPasswordService.updateExpiryTime', { expiryTime: SessionPasswordService.expiryTime });
|
|
|
|
|
}
|
|
|
|
|
static put(pw, file) {
|
|
|
|
|
console.debug('SessionPasswordService.put', { pw, file });
|
|
|
|
|
console.debug(file.parent.path);
|
|
|
|
|
this.cache.put(file.path, pw);
|
|
|
|
|
this.cache.put(file.parent.path, pw);
|
|
|
|
|
this.cache.put(file.basename, pw);
|
|
|
|
|
SessionPasswordService.updateExpiryTime();
|
|
|
|
|
}
|
|
|
|
|
static getExact(file) {
|
|
|
|
|
this.clearIfExpired();
|
|
|
|
|
SessionPasswordService.updateExpiryTime();
|
|
|
|
|
return this.cache.get(file.path, SessionPasswordService.blankPasswordAndHint);
|
|
|
|
|
}
|
|
|
|
|
static getBestGuess(file) {
|
|
|
|
|
this.clearIfExpired();
|
|
|
|
|
//console.debug('SessionPasswordService.getBestGuess', {file})
|
|
|
|
|
SessionPasswordService.updateExpiryTime();
|
|
|
|
|
const buestGuess = this.cache.getFirst([
|
|
|
|
|
file.path,
|
|
|
|
|
file.parent.path,
|
|
|
|
|
file.basename
|
|
|
|
|
], SessionPasswordService.blankPasswordAndHint);
|
|
|
|
|
console.debug('SessionPasswordService.getBestGuess', { file, buestGuess });
|
|
|
|
|
return buestGuess;
|
|
|
|
|
}
|
|
|
|
|
static clearIfExpired() {
|
|
|
|
|
if (SessionPasswordService.expiryTime == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (Date.now() < SessionPasswordService.expiryTime) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.clear();
|
|
|
|
|
}
|
|
|
|
|
static clear() {
|
|
|
|
|
this.cache.clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
SessionPasswordService.isActive = true;
|
|
|
|
|
SessionPasswordService.blankPasswordAndHint = { password: '', hint: '' };
|
|
|
|
|
SessionPasswordService.cache = new MemoryCache();
|
|
|
|
|
SessionPasswordService.baseMinutesToExpire = 0;
|
|
|
|
|
SessionPasswordService.expiryTime = null;
|
|
|
|
|
|
|
|
|
|
class MeldEncryptSettingsTab extends obsidian.PluginSettingTab {
|
|
|
|
|
constructor(app, plugin, settings, features) {
|
|
|
|
|
super(app, plugin);
|
|
|
|
|
this.plugin = plugin;
|
|
|
|
|
this.settings = settings;
|
|
|
|
|
this.features = features;
|
|
|
|
|
}
|
|
|
|
|
display() {
|
|
|
|
|
let { containerEl } = this;
|
|
|
|
|
containerEl.empty();
|
|
|
|
|
containerEl.createEl('h1', { text: 'Settings for Meld Encrypt' });
|
|
|
|
|
// build common settings
|
|
|
|
|
new obsidian.Setting(containerEl)
|
|
|
|
|
.setHeading()
|
|
|
|
|
.setName('Common Settings');
|
|
|
|
|
new obsidian.Setting(containerEl)
|
|
|
|
|
.setName('Confirm password?')
|
|
|
|
|
.setDesc('Confirm password when encrypting.')
|
|
|
|
|
.addToggle(toggle => {
|
|
|
|
|
toggle
|
|
|
|
|
.setValue(this.settings.confirmPassword)
|
|
|
|
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
this.settings.confirmPassword = value;
|
|
|
|
|
yield this.plugin.saveSettings();
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
const updateRememberPasswordSettingsUi = () => {
|
|
|
|
|
if (!this.settings.rememberPassword) {
|
|
|
|
|
pwTimeoutSetting.settingEl.hide();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
pwTimeoutSetting.settingEl.show();
|
|
|
|
|
const rememberPasswordTimeout = this.settings.rememberPasswordTimeout;
|
|
|
|
|
let timeoutString = `${rememberPasswordTimeout} minutes`;
|
|
|
|
|
if (rememberPasswordTimeout == 0) {
|
|
|
|
|
timeoutString = 'Never forget';
|
|
|
|
|
}
|
|
|
|
|
pwTimeoutSetting.setName(`Remember Password Timeout (${timeoutString})`);
|
|
|
|
|
};
|
|
|
|
|
new obsidian.Setting(containerEl)
|
|
|
|
|
.setName('Remember password?')
|
|
|
|
|
.setDesc('Remember the last used passwords when encrypting or decrypting.')
|
|
|
|
|
.addToggle(toggle => {
|
|
|
|
|
toggle
|
|
|
|
|
.setValue(this.settings.rememberPassword)
|
|
|
|
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
this.settings.rememberPassword = value;
|
|
|
|
|
yield this.plugin.saveSettings();
|
|
|
|
|
SessionPasswordService.setActive(this.settings.rememberPassword);
|
|
|
|
|
updateRememberPasswordSettingsUi();
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
const pwTimeoutSetting = new obsidian.Setting(containerEl)
|
|
|
|
|
.setDesc('The number of minutes to remember passwords.')
|
|
|
|
|
.addSlider(slider => {
|
|
|
|
|
slider
|
|
|
|
|
.setLimits(0, 120, 5)
|
|
|
|
|
.setValue(this.settings.rememberPasswordTimeout)
|
|
|
|
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
this.settings.rememberPasswordTimeout = value;
|
|
|
|
|
yield this.plugin.saveSettings();
|
|
|
|
|
SessionPasswordService.setAutoExpire(this.settings.rememberPasswordTimeout);
|
|
|
|
|
updateRememberPasswordSettingsUi();
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
updateRememberPasswordSettingsUi();
|
|
|
|
|
// build feature settings
|
|
|
|
|
this.features.forEach(f => {
|
|
|
|
|
f.buildSettingsUi(containerEl, () => __awaiter(this, void 0, void 0, function* () { return yield this.plugin.saveSettings(); }));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const vectorSize = 16;
|
|
|
|
|
const utf8Encoder = new TextEncoder();
|
|
|
|
|
const utf8Decoder = new TextDecoder();
|
|
|
|
|
const iterations = 1000;
|
|
|
|
|
const salt = utf8Encoder.encode('XHWnDAT6ehMVY2zD');
|
|
|
|
|
class CryptoHelper {
|
|
|
|
|
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;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
encryptToBytes(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);
|
|
|
|
|
return finalBytes;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
convertToString(bytes) {
|
|
|
|
|
let result = '';
|
|
|
|
|
for (let idx = 0; idx < bytes.length; idx++) {
|
|
|
|
|
// append to result
|
|
|
|
|
result += String.fromCharCode(bytes[idx]);
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
encryptToBase64(text, password) {
|
|
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
const finalBytes = yield this.encryptToBytes(text, password);
|
|
|
|
|
//convert array to base64
|
|
|
|
|
const base64Text = btoa(this.convertToString(finalBytes));
|
|
|
|
|
return base64Text;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
stringToArray(str) {
|
|
|
|
|
var result = [];
|
|
|
|
|
for (var i = 0; i < str.length; i++) {
|
|
|
|
|
result.push(str.charCodeAt(i));
|
|
|
|
|
}
|
|
|
|
|
return new Uint8Array(result);
|
|
|
|
|
}
|
|
|
|
|
decryptFromBytes(encryptedBytes, password) {
|
|
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
try {
|
|
|
|
|
// extract iv
|
|
|
|
|
const vector = encryptedBytes.slice(0, vectorSize);
|
|
|
|
|
// extract encrypted text
|
|
|
|
|
const encryptedTextBytes = encryptedBytes.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;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
decryptFromBase64(base64Encoded, password) {
|
|
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
try {
|
|
|
|
|
let bytesToDecode = this.stringToArray(atob(base64Encoded));
|
|
|
|
|
return yield this.decryptFromBytes(bytesToDecode, password);
|
|
|
|
|
// // extract iv
|
|
|
|
|
// const vector = bytesToDecode.slice(0,vectorSize);
|
|
|
|
|
// // extract encrypted text
|
|
|
|
|
// const encryptedTextBytes = bytesToDecode.slice(vectorSize);
|
|
|
|
|
// const key = await this.deriveKey(password);
|
|
|
|
|
// // decrypt into bytes
|
|
|
|
|
// let decryptedBytes = await 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;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* @deprecated
|
|
|
|
|
*/
|
|
|
|
|
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 DecryptModal extends obsidian.Modal {
|
|
|
|
|
constructor(app, title, text = '', showCopyButton) {
|
|
|
|
|
super(app);
|
|
|
|
|
this.decryptInPlace = false;
|
|
|
|
|
this.titleEl.setText(title);
|
|
|
|
|
this.text = text;
|
|
|
|
|
this.showCopyButton = showCopyButton;
|
|
|
|
|
}
|
|
|
|
|
onOpen() {
|
|
|
|
|
let { contentEl } = this;
|
|
|
|
|
let cTextArea;
|
|
|
|
|
const sText = new obsidian.Setting(contentEl)
|
|
|
|
|
.addTextArea(cb => {
|
|
|
|
|
cTextArea = cb;
|
|
|
|
|
cb.setValue(this.text);
|
|
|
|
|
cb.inputEl.setSelectionRange(0, 0);
|
|
|
|
|
cb.inputEl.readOnly = true;
|
|
|
|
|
cb.inputEl.rows = 10;
|
|
|
|
|
cb.inputEl.style.width = '100%';
|
|
|
|
|
cb.inputEl.style.minHeight = '3em';
|
|
|
|
|
cb.inputEl.style.resize = 'vertical';
|
|
|
|
|
});
|
|
|
|
|
sText.settingEl.querySelector('.setting-item-info').remove();
|
|
|
|
|
const sActions = new obsidian.Setting(contentEl);
|
|
|
|
|
if (this.showCopyButton) {
|
|
|
|
|
sActions
|
|
|
|
|
.addButton(cb => {
|
|
|
|
|
cb
|
|
|
|
|
.setButtonText('Copy')
|
|
|
|
|
.onClick(evt => {
|
|
|
|
|
navigator.clipboard.writeText(cTextArea.getValue());
|
|
|
|
|
new obsidian.Notice('Copied!');
|
|
|
|
|
});
|
|
|
|
|
if (!this.showCopyButton) ;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
sActions
|
|
|
|
|
.addButton(cb => {
|
|
|
|
|
cb
|
|
|
|
|
.setWarning()
|
|
|
|
|
.setButtonText('Decrypt in-place')
|
|
|
|
|
.onClick(evt => {
|
|
|
|
|
this.decryptInPlace = true;
|
|
|
|
|
this.close();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class UiHelper {
|
|
|
|
|
/**
|
|
|
|
|
Check if the Settings modal is open
|
|
|
|
|
*/
|
|
|
|
|
static isSettingsModalOpen() {
|
|
|
|
|
return document.querySelector('.mod-settings') !== null;
|
|
|
|
|
}
|
|
|
|
|
static buildPasswordSetting({ container, name, desc = '', autoFocus = false, placeholder = '', initialValue = '', onChangeCallback, onEnterCallback, }) {
|
|
|
|
|
const sControl = new obsidian.Setting(container)
|
|
|
|
|
.setName(name)
|
|
|
|
|
.setDesc(desc)
|
|
|
|
|
.addButton(cb => {
|
|
|
|
|
cb
|
|
|
|
|
.setIcon('reading-glasses')
|
|
|
|
|
.onClick(evt => {
|
|
|
|
|
// toggle view password
|
|
|
|
|
const inputCtrl = sControl.components.find((bc, idx, obj) => bc instanceof obsidian.TextComponent);
|
|
|
|
|
if (inputCtrl instanceof obsidian.TextComponent) {
|
|
|
|
|
inputCtrl.inputEl.type = inputCtrl.inputEl.type == 'password' ? 'text' : 'password';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
.addText(tc => {
|
|
|
|
|
tc.setPlaceholder(placeholder);
|
|
|
|
|
tc.setValue(initialValue);
|
|
|
|
|
tc.inputEl.type = 'password';
|
|
|
|
|
if (onChangeCallback != null) {
|
|
|
|
|
tc.onChange(onChangeCallback);
|
|
|
|
|
}
|
|
|
|
|
if (onEnterCallback != null) {
|
|
|
|
|
tc.inputEl.onkeydown = (ev) => {
|
|
|
|
|
if (ev.key === 'Enter') {
|
|
|
|
|
ev.preventDefault();
|
|
|
|
|
onEnterCallback(tc.getValue());
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
if (autoFocus) {
|
|
|
|
|
setTimeout(() => tc.inputEl.focus(), 0);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return sControl;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class PasswordModal extends obsidian.Modal {
|
|
|
|
|
constructor(app, isEncrypting, confirmPassword, defaultPassword = null, hint = null) {
|
|
|
|
|
super(app);
|
|
|
|
|
// input
|
|
|
|
|
this.defaultPassword = null;
|
|
|
|
|
this.defaultHint = null;
|
|
|
|
|
// output
|
|
|
|
|
this.resultConfirmed = false;
|
|
|
|
|
this.resultPassword = null;
|
|
|
|
|
this.resultHint = null;
|
|
|
|
|
this.defaultPassword = defaultPassword;
|
|
|
|
|
this.confirmPassword = confirmPassword;
|
|
|
|
|
this.isEncrypting = isEncrypting;
|
|
|
|
|
this.defaultHint = hint;
|
|
|
|
|
}
|
|
|
|
|
onOpen() {
|
|
|
|
|
var _a, _b;
|
|
|
|
|
let { contentEl } = this;
|
|
|
|
|
contentEl.empty();
|
|
|
|
|
//this.contentEl.style.width = 'auto';
|
|
|
|
|
this.invalidate();
|
|
|
|
|
let password = (_a = this.defaultPassword) !== null && _a !== void 0 ? _a : '';
|
|
|
|
|
let confirmPass = '';
|
|
|
|
|
let hint = (_b = this.defaultHint) !== null && _b !== void 0 ? _b : '';
|
|
|
|
|
new obsidian.Setting(contentEl).setHeading().setName(this.isEncrypting ? 'Encrypting' : 'Decrypting');
|
|
|
|
|
/* Main password input*/
|
|
|
|
|
UiHelper.buildPasswordSetting({
|
|
|
|
|
container: contentEl,
|
|
|
|
|
name: 'Password:',
|
|
|
|
|
placeholder: this.isEncrypting ? '' : `Hint: ${this.defaultHint}`,
|
|
|
|
|
initialValue: password,
|
|
|
|
|
autoFocus: true,
|
|
|
|
|
onChangeCallback: (value) => {
|
|
|
|
|
password = value;
|
|
|
|
|
this.invalidate();
|
|
|
|
|
},
|
|
|
|
|
onEnterCallback: (value) => {
|
|
|
|
|
password = value;
|
|
|
|
|
this.invalidate();
|
|
|
|
|
if (password.length > 0) {
|
|
|
|
|
if (sConfirmPassword.settingEl.isShown()) {
|
|
|
|
|
//tcConfirmPassword.inputEl.focus();
|
|
|
|
|
const elInp = sConfirmPassword.components.find((bc) => bc instanceof obsidian.TextComponent);
|
|
|
|
|
if (elInp instanceof obsidian.TextComponent) {
|
|
|
|
|
elInp.inputEl.focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (sHint.settingEl.isShown()) {
|
|
|
|
|
//tcHint.inputEl.focus();
|
|
|
|
|
const elInp = sHint.components.find((bc) => bc instanceof obsidian.TextComponent);
|
|
|
|
|
if (elInp instanceof obsidian.TextComponent) {
|
|
|
|
|
elInp.inputEl.focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (validate()) {
|
|
|
|
|
this.close();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
/* End Main password input row */
|
|
|
|
|
/* Confirm password input row */
|
|
|
|
|
const sConfirmPassword = UiHelper.buildPasswordSetting({
|
|
|
|
|
container: contentEl,
|
|
|
|
|
name: 'Confirm Password:',
|
|
|
|
|
onChangeCallback: (value) => {
|
|
|
|
|
confirmPass = value;
|
|
|
|
|
this.invalidate();
|
|
|
|
|
},
|
|
|
|
|
onEnterCallback: (value) => {
|
|
|
|
|
confirmPass = value;
|
|
|
|
|
this.invalidate();
|
|
|
|
|
if (confirmPass.length > 0) {
|
|
|
|
|
if (validate()) {
|
|
|
|
|
if (sHint.settingEl.isShown()) {
|
|
|
|
|
//tcHint.inputEl.focus();
|
|
|
|
|
const elInp = sHint.components.find((bc) => bc instanceof obsidian.TextComponent);
|
|
|
|
|
if (elInp instanceof obsidian.TextComponent) {
|
|
|
|
|
elInp.inputEl.focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
if (!this.confirmPassword) {
|
|
|
|
|
sConfirmPassword.settingEl.hide();
|
|
|
|
|
}
|
|
|
|
|
/* End Confirm password input row */
|
|
|
|
|
/* Hint input row */
|
|
|
|
|
const sHint = new obsidian.Setting(contentEl)
|
|
|
|
|
.setName('Optional Password Hint')
|
|
|
|
|
.addText(tc => {
|
|
|
|
|
//tcHint = tc;
|
|
|
|
|
tc.inputEl.placeholder = `Password Hint`;
|
|
|
|
|
tc.setValue(hint);
|
|
|
|
|
tc.onChange(v => hint = v);
|
|
|
|
|
tc.inputEl.on('keypress', '*', (ev, target) => {
|
|
|
|
|
if (ev.key == 'Enter'
|
|
|
|
|
&& target instanceof HTMLInputElement
|
|
|
|
|
&& target.value.length > 0) {
|
|
|
|
|
ev.preventDefault();
|
|
|
|
|
if (validate()) {
|
|
|
|
|
this.close();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
if (!this.isEncrypting) {
|
|
|
|
|
sHint.settingEl.hide();
|
|
|
|
|
}
|
|
|
|
|
/* END Hint text row */
|
|
|
|
|
new obsidian.Setting(contentEl).addButton(cb => {
|
|
|
|
|
cb
|
|
|
|
|
.setButtonText('Confirm')
|
|
|
|
|
.onClick(evt => {
|
|
|
|
|
if (validate()) {
|
|
|
|
|
this.close();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
const validate = () => {
|
|
|
|
|
this.invalidate();
|
|
|
|
|
sConfirmPassword.setDesc('');
|
|
|
|
|
if (this.confirmPassword) {
|
|
|
|
|
if (password != confirmPass) {
|
|
|
|
|
// passwords don't match
|
|
|
|
|
sConfirmPassword.setDesc('Passwords don\'t match');
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.resultConfirmed = true;
|
|
|
|
|
this.resultPassword = password;
|
|
|
|
|
this.resultHint = hint;
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
invalidate() {
|
|
|
|
|
this.resultConfirmed = false;
|
|
|
|
|
this.resultPassword = null;
|
|
|
|
|
this.resultHint = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const _PREFIX = '%%🔐';
|
|
|
|
|
const _PREFIX_OBSOLETE = _PREFIX + ' ';
|
|
|
|
|
const _PREFIX_A = _PREFIX + 'α ';
|
|
|
|
|
const _SUFFIX = ' 🔐%%';
|
|
|
|
|
const _HINT = '💡';
|
|
|
|
|
class FeatureInplaceEncrypt {
|
|
|
|
|
onload(plugin, settings) {
|
|
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
this.plugin = plugin;
|
|
|
|
|
this.pluginSettings = settings;
|
|
|
|
|
this.featureSettings = settings.featureInplaceEncrypt;
|
|
|
|
|
plugin.addCommand({
|
|
|
|
|
id: 'meld-encrypt',
|
|
|
|
|
name: 'Encrypt/Decrypt',
|
|
|
|
|
icon: 'lock',
|
|
|
|
|
editorCheckCallback: (checking, editor, view) => this.processEncryptDecryptCommand(checking, editor, view, false)
|
|
|
|
|
});
|
|
|
|
|
plugin.addCommand({
|
|
|
|
|
id: 'meld-encrypt-in-place',
|
|
|
|
|
name: 'Encrypt/Decrypt In-place',
|
|
|
|
|
icon: 'lock',
|
|
|
|
|
editorCheckCallback: (checking, editor, view) => this.processEncryptDecryptCommand(checking, editor, view, true)
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
onunload() {
|
|
|
|
|
}
|
|
|
|
|
buildSettingsUi(containerEl, saveSettingCallback) {
|
|
|
|
|
new obsidian.Setting(containerEl)
|
|
|
|
|
.setHeading()
|
|
|
|
|
.setName('In-place Encryption Settings');
|
|
|
|
|
// Selection encrypt feature settings below
|
|
|
|
|
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.featureSettings.expandToWholeLines)
|
|
|
|
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
this.featureSettings.expandToWholeLines = value;
|
|
|
|
|
yield saveSettingCallback();
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
new obsidian.Setting(containerEl)
|
|
|
|
|
.setName('Copy button?')
|
|
|
|
|
.setDesc('Show a button to copy decrypted text.')
|
|
|
|
|
.addToggle(toggle => {
|
|
|
|
|
toggle
|
|
|
|
|
.setValue(this.featureSettings.showCopyButton)
|
|
|
|
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
this.featureSettings.showCopyButton = value;
|
|
|
|
|
yield saveSettingCallback();
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
processEncryptDecryptCommand(checking, editor, view, decryptInPlace) {
|
|
|
|
|
if (checking && UiHelper.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.featureSettings.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, allowEncryption = true) {
|
|
|
|
|
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 (selectionAnalysis.canEncrypt && !allowEncryption) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (checking) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
const activeFile = this.plugin.app.workspace.getActiveFile();
|
|
|
|
|
// Fetch password from user
|
|
|
|
|
// determine default password and hint
|
|
|
|
|
let defaultPassword = '';
|
|
|
|
|
let defaultHint = (_a = selectionAnalysis.decryptable) === null || _a === void 0 ? void 0 : _a.hint;
|
|
|
|
|
if (this.pluginSettings.rememberPassword) {
|
|
|
|
|
const bestGuessPasswordAndHint = SessionPasswordService.getBestGuess(activeFile);
|
|
|
|
|
console.debug({ bestGuessPasswordAndHint });
|
|
|
|
|
defaultPassword = bestGuessPasswordAndHint.password;
|
|
|
|
|
defaultHint = defaultHint !== null && defaultHint !== void 0 ? defaultHint : bestGuessPasswordAndHint.hint;
|
|
|
|
|
}
|
|
|
|
|
const confirmPassword = selectionAnalysis.canEncrypt && this.pluginSettings.confirmPassword;
|
|
|
|
|
const pwModal = new PasswordModal(this.plugin.app, selectionAnalysis.canEncrypt, confirmPassword, defaultPassword, defaultHint);
|
|
|
|
|
pwModal.onClose = () => __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
var _b, _c;
|
|
|
|
|
if (!pwModal.resultConfirmed) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const pw = (_b = pwModal.resultPassword) !== null && _b !== void 0 ? _b : '';
|
|
|
|
|
const hint = (_c = pwModal.resultHint) !== null && _c !== void 0 ? _c : '';
|
|
|
|
|
if (selectionAnalysis.canEncrypt) {
|
|
|
|
|
const encryptable = new Encryptable();
|
|
|
|
|
encryptable.text = selectionText;
|
|
|
|
|
encryptable.hint = hint;
|
|
|
|
|
this.encryptSelection(editor, encryptable, pw, finalSelectionStart, finalSelectionEnd);
|
|
|
|
|
// remember password
|
|
|
|
|
SessionPasswordService.put({ password: pw, hint: hint }, activeFile);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
let decryptSuccess;
|
|
|
|
|
if (selectionAnalysis.decryptable.version == 1) {
|
|
|
|
|
decryptSuccess = yield this.decryptSelection_a(editor, selectionAnalysis.decryptable, pw, finalSelectionStart, finalSelectionEnd, decryptInPlace);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
decryptSuccess = yield this.decryptSelectionObsolete(editor, selectionAnalysis.decryptable, pw, finalSelectionStart, finalSelectionEnd, decryptInPlace);
|
|
|
|
|
}
|
|
|
|
|
// remember password?
|
|
|
|
|
if (decryptSuccess) {
|
|
|
|
|
SessionPasswordService.put({ password: pw, hint: hint }, activeFile);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
pwModal.open();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
encryptSelection(editor, encryptable, password, finalSelectionStart, finalSelectionEnd) {
|
|
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
//encrypt
|
|
|
|
|
const crypto = new CryptoHelper();
|
|
|
|
|
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 CryptoHelper();
|
|
|
|
|
const decryptedText = yield crypto.decryptFromBase64(decryptable.base64CipherText, password);
|
|
|
|
|
if (decryptedText === null) {
|
|
|
|
|
new obsidian.Notice('❌ Decryption failed!');
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
if (decryptInPlace) {
|
|
|
|
|
editor.setSelection(selectionStart, selectionEnd);
|
|
|
|
|
editor.replaceSelection(decryptedText);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
const decryptModal = new DecryptModal(this.plugin.app, '🔓', decryptedText, this.featureSettings.showCopyButton);
|
|
|
|
|
decryptModal.onClose = () => {
|
|
|
|
|
editor.focus();
|
|
|
|
|
if (decryptModal.decryptInPlace) {
|
|
|
|
|
editor.setSelection(selectionStart, selectionEnd);
|
|
|
|
|
editor.replaceSelection(decryptedText);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
decryptModal.open();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
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!');
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
if (decryptInPlace) {
|
|
|
|
|
editor.setSelection(selectionStart, selectionEnd);
|
|
|
|
|
editor.replaceSelection(decryptedText);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
const decryptModal = new DecryptModal(this.plugin.app, '🔓', decryptedText, this.featureSettings.showCopyButton);
|
|
|
|
|
decryptModal.onClose = () => {
|
|
|
|
|
editor.focus();
|
|
|
|
|
if (decryptModal.decryptInPlace) {
|
|
|
|
|
editor.setSelection(selectionStart, selectionEnd);
|
|
|
|
|
editor.replaceSelection(decryptedText);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
decryptModal.open();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
if (content.substring(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;
|
|
|
|
|
}
|
|
|
|
|
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 {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var EncryptedFileContentViewStateEnum;
|
|
|
|
|
(function (EncryptedFileContentViewStateEnum) {
|
|
|
|
|
EncryptedFileContentViewStateEnum[EncryptedFileContentViewStateEnum["init"] = 0] = "init";
|
|
|
|
|
EncryptedFileContentViewStateEnum[EncryptedFileContentViewStateEnum["decryptNote"] = 1] = "decryptNote";
|
|
|
|
|
EncryptedFileContentViewStateEnum[EncryptedFileContentViewStateEnum["editNote"] = 2] = "editNote";
|
|
|
|
|
EncryptedFileContentViewStateEnum[EncryptedFileContentViewStateEnum["changePassword"] = 3] = "changePassword";
|
|
|
|
|
EncryptedFileContentViewStateEnum[EncryptedFileContentViewStateEnum["newNote"] = 4] = "newNote";
|
|
|
|
|
})(EncryptedFileContentViewStateEnum || (EncryptedFileContentViewStateEnum = {}));
|
|
|
|
|
const VIEW_TYPE_ENCRYPTED_FILE_CONTENT = "meld-encrypted-file-content-view";
|
|
|
|
|
class EncryptedFileContentView extends obsidian.TextFileView {
|
|
|
|
|
constructor(leaf) {
|
|
|
|
|
super(leaf);
|
|
|
|
|
// State
|
|
|
|
|
this.currentView = EncryptedFileContentViewStateEnum.init;
|
|
|
|
|
this.encryptionPassword = '';
|
|
|
|
|
this.hint = '';
|
|
|
|
|
this.currentEditorText = '';
|
|
|
|
|
//console.debug('EncryptedFileContentView.constructor', {leaf});
|
|
|
|
|
this.elActionIconLockNote = this.addAction('lock', 'Lock', () => this.actionLockFile());
|
|
|
|
|
this.elActionChangePassword = this.addAction('key', 'Change Password', () => this.actionChangePassword());
|
|
|
|
|
this.contentEl.style.display = 'flex';
|
|
|
|
|
this.contentEl.style.flexDirection = 'column';
|
|
|
|
|
this.contentEl.style.alignItems = 'center';
|
|
|
|
|
}
|
|
|
|
|
actionLockFile() {
|
|
|
|
|
this.encryptionPassword = '';
|
|
|
|
|
this.refreshView(EncryptedFileContentViewStateEnum.decryptNote);
|
|
|
|
|
}
|
|
|
|
|
actionChangePassword() {
|
|
|
|
|
this.refreshView(EncryptedFileContentViewStateEnum.changePassword);
|
|
|
|
|
}
|
|
|
|
|
onPaneMenu(menu, source) {
|
|
|
|
|
//console.debug( {menu, source, 'view': this.currentView});
|
|
|
|
|
if (source == 'tab-header' && this.currentView == EncryptedFileContentViewStateEnum.editNote) {
|
|
|
|
|
menu.addItem(m => {
|
|
|
|
|
m
|
|
|
|
|
.setSection('action')
|
|
|
|
|
.setIcon('lock')
|
|
|
|
|
.setTitle('Lock')
|
|
|
|
|
.onClick(() => this.actionLockFile());
|
|
|
|
|
});
|
|
|
|
|
menu.addItem(m => {
|
|
|
|
|
m
|
|
|
|
|
.setSection('action')
|
|
|
|
|
.setIcon('key')
|
|
|
|
|
.setTitle('Change Password')
|
|
|
|
|
.onClick(() => this.actionChangePassword());
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
super.onPaneMenu(menu, source);
|
|
|
|
|
}
|
|
|
|
|
createTitle(title) {
|
|
|
|
|
return this.contentEl.createDiv({
|
|
|
|
|
text: `🔐 ${title} 🔐`,
|
|
|
|
|
attr: {
|
|
|
|
|
style: 'margin-bottom:2em;'
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
validatePassword(pw) {
|
|
|
|
|
if (pw.length == 0) {
|
|
|
|
|
return 'Password is too short';
|
|
|
|
|
}
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
validateConfirm(pw, cpw) {
|
|
|
|
|
const passwordMatch = pw === cpw;
|
|
|
|
|
return passwordMatch ? '' : 'Password doesn\'t match';
|
|
|
|
|
}
|
|
|
|
|
createNewNoteView() {
|
|
|
|
|
//console.debug('createDecryptNoteView', { "hint": this.hint} );
|
|
|
|
|
const container = this.createInputContainer();
|
|
|
|
|
new obsidian.Setting(container)
|
|
|
|
|
.setDesc('Please provide a password and hint to start editing this note.');
|
|
|
|
|
const submit = (password, confirm, hint) => __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
var validPw = this.validatePassword(password);
|
|
|
|
|
var validCpw = this.validateConfirm(password, confirm);
|
|
|
|
|
sPassword.setDesc(validPw);
|
|
|
|
|
sConfirm.setDesc(validCpw);
|
|
|
|
|
if (validPw.length === 0 && validCpw.length === 0) {
|
|
|
|
|
//set password and hint and open note
|
|
|
|
|
this.encryptionPassword = password;
|
|
|
|
|
this.hint = hint;
|
|
|
|
|
this.currentEditorText = this.file.basename;
|
|
|
|
|
yield this.encodeAndSave();
|
|
|
|
|
SessionPasswordService.put({ password: password, hint: hint }, this.file);
|
|
|
|
|
this.refreshView(EncryptedFileContentViewStateEnum.editNote);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
const bestGuessPassAndHint = SessionPasswordService.getBestGuess(this.file);
|
|
|
|
|
let password = bestGuessPassAndHint.password;
|
|
|
|
|
let confirm = '';
|
|
|
|
|
let hint = bestGuessPassAndHint.hint;
|
|
|
|
|
const sPassword = UiHelper.buildPasswordSetting({
|
|
|
|
|
container,
|
|
|
|
|
name: 'Password:',
|
|
|
|
|
autoFocus: true,
|
|
|
|
|
initialValue: password,
|
|
|
|
|
onChangeCallback: (value) => {
|
|
|
|
|
password = value;
|
|
|
|
|
sPassword.setDesc(this.validatePassword(password));
|
|
|
|
|
sConfirm.setDesc(this.validateConfirm(password, confirm));
|
|
|
|
|
},
|
|
|
|
|
onEnterCallback: (value) => {
|
|
|
|
|
password = value;
|
|
|
|
|
if (password.length > 0) {
|
|
|
|
|
sConfirm.controlEl.querySelector('input').focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
const sConfirm = UiHelper.buildPasswordSetting({
|
|
|
|
|
container,
|
|
|
|
|
name: 'Confirm:',
|
|
|
|
|
autoFocus: false,
|
|
|
|
|
onChangeCallback: (value) => {
|
|
|
|
|
confirm = value;
|
|
|
|
|
sPassword.setDesc(this.validatePassword(password));
|
|
|
|
|
sConfirm.setDesc(this.validateConfirm(password, confirm));
|
|
|
|
|
},
|
|
|
|
|
onEnterCallback: (value) => {
|
|
|
|
|
confirm = value;
|
|
|
|
|
const passwordMatch = password === confirm;
|
|
|
|
|
if (passwordMatch) {
|
|
|
|
|
sHint.controlEl.querySelector('input').focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
const sHint = new obsidian.Setting(container)
|
|
|
|
|
.setName("Hint:")
|
|
|
|
|
.addText((tc) => {
|
|
|
|
|
tc.setValue(hint);
|
|
|
|
|
tc.onChange(v => {
|
|
|
|
|
hint = v;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
sHint.controlEl.on('keydown', '*', (ev) => {
|
|
|
|
|
if (ev.key === 'Enter') {
|
|
|
|
|
ev.preventDefault();
|
|
|
|
|
submit(password, confirm, hint);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
new obsidian.Setting(container)
|
|
|
|
|
.addButton(bc => {
|
|
|
|
|
bc
|
|
|
|
|
.setCta()
|
|
|
|
|
.setIcon('go-to-file')
|
|
|
|
|
.setTooltip('Edit')
|
|
|
|
|
.onClick((ev) => submit(password, confirm, hint));
|
|
|
|
|
});
|
|
|
|
|
return container;
|
|
|
|
|
}
|
|
|
|
|
createDecryptNoteView() {
|
|
|
|
|
const container = this.createInputContainer();
|
|
|
|
|
new obsidian.Setting(container)
|
|
|
|
|
.setDesc('Please provide a password to unlock this note.');
|
|
|
|
|
const bestGuessPassAndHint = SessionPasswordService.getBestGuess(this.file);
|
|
|
|
|
this.encryptionPassword = bestGuessPassAndHint.password;
|
|
|
|
|
UiHelper.buildPasswordSetting({
|
|
|
|
|
container,
|
|
|
|
|
name: 'Password:',
|
|
|
|
|
initialValue: this.encryptionPassword,
|
|
|
|
|
autoFocus: true,
|
|
|
|
|
placeholder: this.formatHint(this.hint),
|
|
|
|
|
onChangeCallback: (value) => {
|
|
|
|
|
this.encryptionPassword = value;
|
|
|
|
|
},
|
|
|
|
|
onEnterCallback: () => __awaiter(this, void 0, void 0, function* () { return yield this.handleDecryptButtonClick(); })
|
|
|
|
|
});
|
|
|
|
|
new obsidian.Setting(container)
|
|
|
|
|
.addButton(bc => {
|
|
|
|
|
bc
|
|
|
|
|
.setCta()
|
|
|
|
|
.setIcon('checkmark')
|
|
|
|
|
.setTooltip('Unlock & Edit')
|
|
|
|
|
.onClick((evt) => this.handleDecryptButtonClick());
|
|
|
|
|
});
|
|
|
|
|
return container;
|
|
|
|
|
}
|
|
|
|
|
encodeAndSave() {
|
|
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
try {
|
|
|
|
|
//console.debug('encodeAndSave');
|
|
|
|
|
var fileData = yield FileDataHelper.encode(this.encryptionPassword, this.hint, this.currentEditorText);
|
|
|
|
|
this.data = JsonFileEncoding.encode(fileData);
|
|
|
|
|
this.requestSave();
|
|
|
|
|
}
|
|
|
|
|
catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
new obsidian.Notice(e, 10000);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
createEditorView() {
|
|
|
|
|
//const container = this.contentEl.createEl('textarea');
|
|
|
|
|
const container = this.contentEl.createDiv();
|
|
|
|
|
container.contentEditable = 'true';
|
|
|
|
|
container.style.flexGrow = '1';
|
|
|
|
|
container.style.alignSelf = 'stretch';
|
|
|
|
|
//container.value = this.currentEditorText
|
|
|
|
|
container.innerText = this.currentEditorText;
|
|
|
|
|
container.focus();
|
|
|
|
|
container.on('input', '*', (ev, target) => __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
//console.debug('editor input',{ev, target});
|
|
|
|
|
//this.currentEditorText = container.value;
|
|
|
|
|
this.currentEditorText = container.innerText;
|
|
|
|
|
yield this.encodeAndSave();
|
|
|
|
|
}));
|
|
|
|
|
return container;
|
|
|
|
|
}
|
|
|
|
|
createInputContainer() {
|
|
|
|
|
return this.contentEl.createDiv({
|
|
|
|
|
'attr': {
|
|
|
|
|
'style': 'width:100%; max-width:400px;'
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
createChangePasswordView() {
|
|
|
|
|
const container = this.createInputContainer();
|
|
|
|
|
let newPassword = '';
|
|
|
|
|
let confirm = '';
|
|
|
|
|
let newHint = '';
|
|
|
|
|
const submit = (newPassword, confirm, newHint) => __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
var validPw = this.validatePassword(newPassword);
|
|
|
|
|
var validCpw = this.validateConfirm(newPassword, confirm);
|
|
|
|
|
sNewPassword.setDesc(validPw);
|
|
|
|
|
sConfirm.setDesc(validCpw);
|
|
|
|
|
if (validPw.length === 0 && validCpw.length === 0) {
|
|
|
|
|
//set password and hint and open note
|
|
|
|
|
//console.debug('createChangePasswordView submit');
|
|
|
|
|
this.encryptionPassword = newPassword;
|
|
|
|
|
this.hint = newHint;
|
|
|
|
|
this.encodeAndSave();
|
|
|
|
|
this.refreshView(EncryptedFileContentViewStateEnum.editNote);
|
|
|
|
|
new obsidian.Notice('Password and Hint were changed');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
const sNewPassword = UiHelper.buildPasswordSetting({
|
|
|
|
|
container,
|
|
|
|
|
name: 'New Password:',
|
|
|
|
|
autoFocus: true,
|
|
|
|
|
onChangeCallback: (value) => {
|
|
|
|
|
newPassword = value;
|
|
|
|
|
sNewPassword.setDesc(this.validatePassword(newPassword));
|
|
|
|
|
sConfirm.setDesc(this.validateConfirm(newPassword, confirm));
|
|
|
|
|
},
|
|
|
|
|
onEnterCallback: (value) => {
|
|
|
|
|
newPassword = value;
|
|
|
|
|
if (newPassword.length > 0) {
|
|
|
|
|
sConfirm.controlEl.querySelector('input').focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
const sConfirm = UiHelper.buildPasswordSetting({
|
|
|
|
|
container,
|
|
|
|
|
name: 'Confirm:',
|
|
|
|
|
onChangeCallback: (value) => {
|
|
|
|
|
confirm = value;
|
|
|
|
|
sNewPassword.setDesc(this.validatePassword(newPassword));
|
|
|
|
|
sConfirm.setDesc(this.validateConfirm(newPassword, confirm));
|
|
|
|
|
},
|
|
|
|
|
onEnterCallback: (value) => {
|
|
|
|
|
confirm = value;
|
|
|
|
|
// validate confirm
|
|
|
|
|
const passwordMatch = newPassword === confirm;
|
|
|
|
|
if (passwordMatch) {
|
|
|
|
|
sHint.controlEl.querySelector('input').focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
const sHint = new obsidian.Setting(container)
|
|
|
|
|
.setName("New Hint:")
|
|
|
|
|
.addText((tc) => {
|
|
|
|
|
tc.onChange(v => {
|
|
|
|
|
newHint = v;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
sHint.controlEl.on('keydown', '*', (ev) => {
|
|
|
|
|
if (ev.key === 'Enter') {
|
|
|
|
|
ev.preventDefault();
|
|
|
|
|
submit(newPassword, confirm, newHint);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
new obsidian.Setting(container)
|
|
|
|
|
.addButton(bc => {
|
|
|
|
|
bc
|
|
|
|
|
.removeCta()
|
|
|
|
|
.setIcon('cross')
|
|
|
|
|
//.setButtonText('Cancel')
|
|
|
|
|
.setTooltip('Cancel')
|
|
|
|
|
.onClick(() => {
|
|
|
|
|
this.refreshView(EncryptedFileContentViewStateEnum.editNote);
|
|
|
|
|
});
|
|
|
|
|
}).addButton(bc => {
|
|
|
|
|
bc
|
|
|
|
|
.setCta()
|
|
|
|
|
.setIcon('checkmark')
|
|
|
|
|
.setTooltip('Change Password')
|
|
|
|
|
//.setButtonText('Change Password')
|
|
|
|
|
.setWarning()
|
|
|
|
|
.onClick((ev) => {
|
|
|
|
|
submit(newPassword, confirm, newHint);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
return container;
|
|
|
|
|
}
|
|
|
|
|
formatHint(hint) {
|
|
|
|
|
if (hint.length > 0) {
|
|
|
|
|
return `Hint: ${hint}`;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
refreshView(newView) {
|
|
|
|
|
//console.debug('refreshView',{'currentView':this.currentView, newView});
|
|
|
|
|
this.elActionIconLockNote.hide();
|
|
|
|
|
this.elActionChangePassword.hide();
|
|
|
|
|
// clear view
|
|
|
|
|
this.contentEl.empty();
|
|
|
|
|
this.currentView = newView;
|
|
|
|
|
switch (this.currentView) {
|
|
|
|
|
case EncryptedFileContentViewStateEnum.newNote:
|
|
|
|
|
this.createTitle('This note will be encrypted');
|
|
|
|
|
this.createNewNoteView();
|
|
|
|
|
break;
|
|
|
|
|
case EncryptedFileContentViewStateEnum.decryptNote:
|
|
|
|
|
this.createTitle('This note is encrypted');
|
|
|
|
|
this.createDecryptNoteView();
|
|
|
|
|
break;
|
|
|
|
|
case EncryptedFileContentViewStateEnum.editNote:
|
|
|
|
|
this.elActionIconLockNote.show();
|
|
|
|
|
this.elActionChangePassword.show();
|
|
|
|
|
this.createTitle('This note is encrypted');
|
|
|
|
|
this.createEditorView();
|
|
|
|
|
break;
|
|
|
|
|
case EncryptedFileContentViewStateEnum.changePassword:
|
|
|
|
|
this.createTitle('Change encrypted note password');
|
|
|
|
|
this.createChangePasswordView();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
handleDecryptButtonClick() {
|
|
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
var fileData = JsonFileEncoding.decode(this.data);
|
|
|
|
|
//console.debug('Decrypt button', fileData);
|
|
|
|
|
const decryptedText = yield FileDataHelper.decrypt(fileData, this.encryptionPassword);
|
|
|
|
|
if (decryptedText === null) {
|
|
|
|
|
new obsidian.Notice('Decryption failed');
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
SessionPasswordService.put({ password: this.encryptionPassword, hint: this.hint }, this.file);
|
|
|
|
|
this.currentEditorText = decryptedText;
|
|
|
|
|
this.refreshView(EncryptedFileContentViewStateEnum.editNote);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// important
|
|
|
|
|
canAcceptExtension(extension) {
|
|
|
|
|
//console.debug('EncryptedFileContentView.canAcceptExtension', {extension});
|
|
|
|
|
return extension == 'encrypted';
|
|
|
|
|
}
|
|
|
|
|
// important
|
|
|
|
|
getViewType() {
|
|
|
|
|
return VIEW_TYPE_ENCRYPTED_FILE_CONTENT;
|
|
|
|
|
}
|
|
|
|
|
// the data to show on the view
|
|
|
|
|
setViewData(data, clear) {
|
|
|
|
|
// console.debug('EncryptedFileContentView.setViewData', {
|
|
|
|
|
// data,
|
|
|
|
|
// clear,
|
|
|
|
|
// 'pass':this.encryptionPassword,
|
|
|
|
|
// //'mode':this.getMode(),
|
|
|
|
|
// //'mode-data':this.currentMode.get(),
|
|
|
|
|
// //'preview-mode-data':this.previewMode.get()
|
|
|
|
|
// });
|
|
|
|
|
if (clear) {
|
|
|
|
|
var newView;
|
|
|
|
|
if (data === '') {
|
|
|
|
|
// blank new file
|
|
|
|
|
newView = EncryptedFileContentViewStateEnum.newNote;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
newView = EncryptedFileContentViewStateEnum.decryptNote;
|
|
|
|
|
}
|
|
|
|
|
// new file, we don't know what the password is yet
|
|
|
|
|
this.encryptionPassword = '';
|
|
|
|
|
// json decode file data to get the Hint
|
|
|
|
|
var fileData = JsonFileEncoding.decode(this.data);
|
|
|
|
|
this.hint = fileData.hint;
|
|
|
|
|
this.refreshView(newView);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
this.leaf.detach();
|
|
|
|
|
new obsidian.Notice('Multiple views of the same encrypted note isn\'t supported');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// the data to save to disk
|
|
|
|
|
getViewData() {
|
|
|
|
|
// console.debug('EncryptedFileContentView.getViewData', {
|
|
|
|
|
// 'this':this,
|
|
|
|
|
// 'data':this.data,
|
|
|
|
|
// });
|
|
|
|
|
return this.data;
|
|
|
|
|
}
|
|
|
|
|
clear() {
|
|
|
|
|
//console.debug('EncryptedFileContentView.clear');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
class FileData {
|
|
|
|
|
constructor(hint, encodedData) {
|
|
|
|
|
this.version = "1.0";
|
|
|
|
|
this.hint = hint;
|
|
|
|
|
this.encodedData = encodedData;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
class FileDataHelper {
|
|
|
|
|
static encode(pass, hint, text) {
|
|
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
const crypto = new CryptoHelper();
|
|
|
|
|
const encryptedData = yield crypto.encryptToBase64(text, pass);
|
|
|
|
|
return new FileData(hint, encryptedData);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
static decrypt(data, pass) {
|
|
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
if (data.encodedData == '') {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
const crypto = new CryptoHelper();
|
|
|
|
|
return yield crypto.decryptFromBase64(data.encodedData, pass);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
class JsonFileEncoding {
|
|
|
|
|
static encode(data) {
|
|
|
|
|
return JSON.stringify(data, null, 2);
|
|
|
|
|
}
|
|
|
|
|
static decode(encodedText) {
|
|
|
|
|
//console.debug('JsonFileEncoding.decode',{encodedText});
|
|
|
|
|
if (encodedText === '') {
|
|
|
|
|
return new FileData("", "");
|
|
|
|
|
}
|
|
|
|
|
return JSON.parse(encodedText);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class FeatureWholeNoteEncrypt {
|
|
|
|
|
onload(plugin, settings) {
|
|
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
this.plugin = plugin;
|
|
|
|
|
this.settings = settings.featureWholeNoteEncrypt;
|
|
|
|
|
this.updateUiForSettings();
|
|
|
|
|
this.plugin.registerView(VIEW_TYPE_ENCRYPTED_FILE_CONTENT, (leaf) => new EncryptedFileContentView(leaf));
|
|
|
|
|
this.plugin.registerExtensions(['encrypted'], VIEW_TYPE_ENCRYPTED_FILE_CONTENT);
|
|
|
|
|
this.plugin.addCommand({
|
|
|
|
|
id: 'meld-encrypt-create-new-note',
|
|
|
|
|
name: 'Create new encrypted note',
|
|
|
|
|
icon: 'lock',
|
|
|
|
|
checkCallback: (checking) => this.processCreateNewEncryptedNoteCommand(checking)
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
onunload() {
|
|
|
|
|
this.plugin.app.workspace.detachLeavesOfType(VIEW_TYPE_ENCRYPTED_FILE_CONTENT);
|
|
|
|
|
}
|
|
|
|
|
processCreateNewEncryptedNoteCommand(checking) {
|
|
|
|
|
//console.debug('processCreateNewEncryptedNoteCommand', {checking});
|
|
|
|
|
try {
|
|
|
|
|
if (checking || UiHelper.isSettingsModalOpen()) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
let newFilename = obsidian.moment().format('[Untitled] YYYYMMDD hhmmss[.encrypted]');
|
|
|
|
|
let newFileFolder;
|
|
|
|
|
const activeFile = this.plugin.app.workspace.getActiveFile();
|
|
|
|
|
if (activeFile != null) {
|
|
|
|
|
newFileFolder = this.plugin.app.fileManager.getNewFileParent(activeFile.path);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
newFileFolder = this.plugin.app.fileManager.getNewFileParent('');
|
|
|
|
|
}
|
|
|
|
|
const newFilepath = obsidian.normalizePath(newFileFolder.path + "/" + newFilename);
|
|
|
|
|
//console.debug('processCreateNewEncryptedNoteCommand', {newFilepath});
|
|
|
|
|
this.plugin.app.vault.create(newFilepath, '').then(f => {
|
|
|
|
|
const leaf = this.plugin.app.workspace.getLeaf(false);
|
|
|
|
|
leaf.openFile(f);
|
|
|
|
|
}).catch(reason => {
|
|
|
|
|
new obsidian.Notice(reason, 10000);
|
|
|
|
|
});
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
new obsidian.Notice(e, 10000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
buildSettingsUi(containerEl, saveSettingCallback) {
|
|
|
|
|
new obsidian.Setting(containerEl)
|
|
|
|
|
.setHeading()
|
|
|
|
|
.setName('Whole Note Encryption Settings');
|
|
|
|
|
new obsidian.Setting(containerEl)
|
|
|
|
|
.setName('Add ribbon icon to create note')
|
|
|
|
|
.setDesc('Adds a ribbon icon to the left bar to create an encrypted note.')
|
|
|
|
|
.addToggle(toggle => {
|
|
|
|
|
toggle
|
|
|
|
|
.setValue(this.settings.addRibbonIconToCreateNote)
|
|
|
|
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
this.settings.addRibbonIconToCreateNote = value;
|
|
|
|
|
yield saveSettingCallback();
|
|
|
|
|
this.updateUiForSettings();
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
updateUiForSettings() {
|
|
|
|
|
if (this.settings.addRibbonIconToCreateNote) {
|
|
|
|
|
// turn on ribbon icon
|
|
|
|
|
if (this.ribbonIconCreateNewNote == null) {
|
|
|
|
|
this.ribbonIconCreateNewNote = this.plugin.addRibbonIcon('lock', 'Create new encrypted note', (ev) => {
|
|
|
|
|
this.processCreateNewEncryptedNoteCommand(false);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
// turn off ribbon icon
|
|
|
|
|
if (this.ribbonIconCreateNewNote != null) {
|
|
|
|
|
this.ribbonIconCreateNewNote.remove();
|
|
|
|
|
this.ribbonIconCreateNewNote = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class MeldEncrypt extends obsidian.Plugin {
|
|
|
|
|
constructor() {
|
|
|
|
|
super(...arguments);
|
|
|
|
|
this.enabledFeatures = [];
|
|
|
|
|
}
|
|
|
|
|
onload() {
|
|
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
// Settings
|
|
|
|
|
yield this.loadSettings();
|
|
|
|
|
this.enabledFeatures.push(new FeatureWholeNoteEncrypt(), new FeatureInplaceEncrypt());
|
|
|
|
|
this.addSettingTab(new MeldEncryptSettingsTab(this.app, this, this.settings, this.enabledFeatures));
|
|
|
|
|
// End Settings
|
|
|
|
|
// load features
|
|
|
|
|
this.enabledFeatures.forEach((f) => __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
yield f.onload(this, this.settings);
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
onunload() {
|
|
|
|
|
this.enabledFeatures.forEach((f) => __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
f.onunload();
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
loadSettings() {
|
|
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
const DEFAULT_SETTINGS = {
|
|
|
|
|
confirmPassword: true,
|
|
|
|
|
rememberPassword: true,
|
|
|
|
|
rememberPasswordTimeout: 30,
|
|
|
|
|
featureWholeNoteEncrypt: {
|
|
|
|
|
addRibbonIconToCreateNote: true,
|
|
|
|
|
},
|
|
|
|
|
featureInplaceEncrypt: {
|
|
|
|
|
expandToWholeLines: false,
|
|
|
|
|
showCopyButton: true,
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
this.settings = Object.assign(DEFAULT_SETTINGS, yield this.loadData());
|
|
|
|
|
// apply settings
|
|
|
|
|
SessionPasswordService.setActive(this.settings.rememberPassword);
|
|
|
|
|
SessionPasswordService.setAutoExpire(this.settings.rememberPasswordTimeout == 0
|
|
|
|
|
? null
|
|
|
|
|
: this.settings.rememberPasswordTimeout);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
saveSettings() {
|
|
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
|
|
yield this.saveData(this.settings);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = MeldEncrypt;
|
|
|
|
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFpbi5qcyIsInNvdXJjZXMiOlsiLi4vbm9kZV9tb2R1bGVzL3RzbGliL3RzbGliLmVzNi5qcyIsIi4uL3NyYy9zZXJ2aWNlcy9NZW1vcnlDYWNoZS50cyIsIi4uL3NyYy9zZXJ2aWNlcy9TZXNzaW9uUGFzc3dvcmRTZXJ2aWNlLnRzIiwiLi4vc3JjL3NldHRpbmdzL01lbGRFbmNyeXB0U2V0dGluZ3NUYWIudHMiLCIuLi9zcmMvc2VydmljZXMvQ3J5cHRvSGVscGVyLnRzIiwiLi4vc3JjL3NlcnZpY2VzL0NyeXB0b0hlbHBlck9ic29sZXRlLnRzIiwiLi4vc3JjL2ZlYXR1cmVzL2ZlYXR1cmUtaW5wbGFjZS1lbmNyeXB0L0RlY3J5cHRNb2RhbC50cyIsIi4uL3NyYy9zZXJ2aWNlcy9VaUhlbHBlci50cyIsIi4uL3NyYy9mZWF0dXJlcy9mZWF0dXJlLWlucGxhY2UtZW5jcnlwdC9QYXNzd29yZE1vZGFsLnRzIiwiLi4vc3JjL2ZlYXR1cmVzL2ZlYXR1cmUtaW5wbGFjZS1lbmNyeXB0L0ZlYXR1cmVJbnBsYWNlRW5jcnlwdC50cyIsIi4uL3NyYy9mZWF0dXJlcy9mZWF0dXJlLXdob2xlLW5vdGUtZW5jcnlwdC9FbmNyeXB0ZWRGaWxlQ29udGVudFZpZXcudHMiLCIuLi9zcmMvZmVhdHVyZXMvZmVhdHVyZS13aG9sZS1ub3RlLWVuY3J5cHQvRmVhdHVyZVdob2xlTm90ZUVuY3J5cHQudHMiLCIuLi9zcmMvbWFpbi50cyJdLCJzb3VyY2VzQ29udGVudCI6WyIvKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqXHJcbkNvcHlyaWdodCAoYykgTWljcm9zb2Z0IENvcnBvcmF0aW9uLlxyXG5cclxuUGVybWlzc2lvbiB0byB1c2UsIGNvcHksIG1vZGlmeSwgYW5kL29yIGRpc3RyaWJ1dGUgdGhpcyBzb2Z0d2FyZSBmb3IgYW55XHJcbnB1cnBvc2Ugd2l0aCBvciB3aXRob3V0IGZlZSBpcyBoZXJlYnkgZ3JhbnRlZC5cclxuXHJcblRIRSBTT0ZUV0FSRSBJUyBQUk9WSURFRCBcIkFTIElTXCIgQU5EIFRIRSBBVVRIT1IgRElTQ0xBSU1TIEFMTCBXQVJSQU5USUVTIFdJVEhcclxuUkVHQVJEIFRPIFRISVMgU09GVFdBUkUgSU5DTFVESU5HIEFMTCBJTVBMSUVEIFdBUlJBTlRJRVMgT0YgTUVSQ0hBTlRBQklMSVRZXHJcbkFORCBGSVRORVNTLiBJTiBOTyBFVkVOVCBTSEFMTCBUSEUgQVVUSE9SIEJFIExJQUJMRSBGT1IgQU5ZIFNQRUNJQUwsIERJUkVDVCxcclxuSU5ESVJFQ1QsIE9SIENPTlNFUVVFTlRJQUwgREFNQUdFUyBPUiBBTlkgREFNQUdFUyBXSEFUU09FVkVSIFJFU1VMVElORyBGUk9NXHJcbkxPU1MgT0YgVVNFLCBEQVRBIE9SIFBST0ZJVFMsIFdIRVRIRVIgSU4gQU4gQUNUSU9OIE9GIENPTlRSQUNULCBORUdMSUdFTkNFIE9SXHJcbk9USEVSIFRPUlRJT1VTIEFDVElPTiwgQVJJU0lORyBPVVQgT0YgT1IgSU4gQ09OTkVDVElPTiBXSVRIIFRIRSBVU0UgT1JcclxuUEVSRk9STUFOQ0UgT0YgVEhJUyBTT0ZUV0FSRS5cclxuKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKiogKi9cclxuLyogZ2xvYmFsIFJlZmxlY3QsIFByb21pc2UgKi9cclxuXHJcbnZhciBleHRlbmRTdGF0aWNzID0gZnVuY3Rpb24oZCwgYikge1xyXG4gICAgZXh0ZW5kU3RhdGljcyA9IE9iamVjdC5zZXRQcm90b3R5cGVPZiB8fFxyXG4gICAgICAgICh7IF9fcHJvdG9fXzogW10gfSBpbnN0YW5jZW9mIEFycmF5ICYmIGZ1bmN0aW9uIChkLCBiKSB7IGQuX19wcm90b19fID0gYjsgfSkgfHxcclxuICAgICAgICBmdW5jdGlvbiAoZCwgYikgeyBmb3IgKHZhciBwIGluIGIpIGlmIChPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwoYiwgcCkpIGRbcF0gPSBiW3BdOyB9O1xyXG4gICAgcmV0dXJuIGV4dGVuZFN0YXRpY3MoZCwgYik7XHJcbn07XHJcblxyXG5leHBvcnQgZnVuY3Rpb24gX19leHRlbmRzKGQsIGIpIHtcclxuICAgIGlmICh0eXBlb2YgYiAhPT0gXCJmdW5jdGlvblwiICYmIGIgIT09IG51bGwpXHJcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihcIkNsYXNzIGV4dGVuZHMgdmFsdWUgXCIgKyBTdHJpbmcoYikgKyBcIiBpcyBub3QgYSBjb25zdHJ1Y3RvciBvciBudWxsXCIpO1xyXG4gICAgZXh0ZW5kU3RhdGljcyhkLCBiKTtcclxuICAgIGZ1bmN0aW9uIF9fKCkgeyB0aGlzLmNvbnN0cnVjdG9yID0gZDsgfVxyXG4gICAgZC5wcm90b3R5cGUgPSBiID09PSBudWxsID8gT2JqZWN0LmNyZWF0ZShiKSA6IChfXy5wcm90b3R5cGUgPSBiLnByb3RvdHlwZSwgbmV3IF9fKCkpO1xyXG59XHJcblxyXG5leHBvcnQgdmFyIF9fYXNzaWduID0gZnVuY3Rpb24oKSB7XHJcbiAgICBfX2Fzc2lnbiA9IE9iamVjdC5hc3NpZ24gfHwgZnVuY3Rpb24gX19hc3NpZ24odCkge1xyXG4gICAgICAgIGZvciAodmFyIHMsIGkgPSAxLCBuID0gYXJndW1lbnRzLmxlbmd0aDsgaSA8IG47IGkrKykge1xyXG4gICAgICAgICAgICBzID0gYXJndW1lbnRzW2ldO1xyXG4gICAgICAgICAgICBmb3IgKHZhciBwIGluIHMpIGlmIChPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwocywgcCkpIHRbcF0gPSBzW3BdO1xyXG4gICAgICAgIH1cclxuICAgICAgICByZXR1cm4gdDtcclxuICAgIH1cclxuICAgIHJldHVybiBfX2Fzc2lnbi5hcHBseSh0aGlzLCBhcmd1bWVudHMpO1xyXG59XHJcblxyXG5leHBvcnQgZnVuY3Rpb24gX19yZXN0KHMsIGUpIHtcclxuICAgIHZhciB0ID0ge307XHJcbiAgICBmb3IgKHZhciBwIGluIHMpIGlmIChPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwocywgcCkgJiYgZS5pbmRleE9mKHApIDwgMClcclxuICAgICAgICB0W3BdID0gc1twXTtcclxuICAgIGlmIChzICE9IG51bGwgJiYgdHlwZW9mIE9iamVjdC5nZXRPd25Qcm9wZXJ0eVN5bWJvbHMgPT09IFwiZnVuY3Rpb25cIilcclxuICAgICAgICBmb3IgKHZhciBpID0gMCwgcCA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eVN5bWJvbHMocyk7IGkgPCBwLmxlbmd0aDsgaSsrKSB7XHJcbiAgICAgICAgICAgIGlmIChlLmluZGV4T2YocFtpXSkgPCAwICYmIE9
|