Skip to content

Commit

Permalink
removed crypto-es, use crypto.subtle, clean up
Browse files Browse the repository at this point in the history
  • Loading branch information
meld-cp committed Feb 18, 2021
1 parent dfe34f3 commit 5cb2b7d
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 77 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Hide secrets in your [Obsidian.md](https://obsidian.md/) notes.

This plugin allows you to encrypt portions of your notes allowing you to store sensitive details along with other information.

Under the hood it uses the Advanced Encryption Standard (AES) cipher from the [Crypro-ES](https://github.com/entronad/crypto-es) module.
Under the hood it uses the Advanced Encryption Standard (AES) in GCM mode.

> NOTE: Your passwords are never stored anywhere, if you forget your password you can't decrypt your text.
Expand Down Expand Up @@ -53,3 +53,6 @@ Thank you for your support 🙏

<a href="https://www.buymeacoffee.com/cleon"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=cleon&button_colour=40DCA5&font_colour=ffffff&font_family=Cookie&outline_colour=000000&coffee_colour=FFDD00"></a>


## More info
- [Crypto details](docs/crypto-details.md)
33 changes: 33 additions & 0 deletions docs/crypto-details.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Decrypting without Obsidian

Here are further details in case you ever need to decrypt snippets without Obsidian and this plugin.

The plugin uses the SubtleCrypto interface of the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto).

The result of the AES-GCM encryption is surrounded by markers and Base64 encoded so it can be shown in notes, for example:

```
%%🔐 iWPmKEJm7dzCJze3p6TAzVv+F2kYh29kd3FXyOEmHiU= 🔐%%
```

After stripping the prefix (%%🔐 ) and suffix ( 🔐%%) from the text, you'll need to convert the base64 encoding back to an array of bytes. Form here, you can decrypt using:
```js
const decryptedBytes = crypto.subtle.decrypt(algorithm, key, bytesToDecrypt)
```
where:
```js
const algorithm = {
name: 'AES-GCM',
iv: new Uint8Array([196, 190, 240, 190, 188, 78, 41, 132, 15, 220, 84, 211]),
tagLength: 128
}
//See: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt

const key = await crypto.subtle.importKey(
'raw',
await crypto.subtle.digest({ name: 'SHA-256' }, new TextEncoder().encode(password)),
algorithm,
false,
['encrypt', 'decrypt']
);
```
3 changes: 0 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,5 @@
"rollup": "^2.32.1",
"tslib": "^2.0.3",
"typescript": "^4.0.3"
},
"dependencies": {
"crypto-es": "^1.2.7"
}
}
71 changes: 71 additions & 0 deletions src/CryptoHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@

const algorithm = {
name: 'AES-GCM',
iv: new Uint8Array([196, 190, 240, 190, 188, 78, 41, 132, 15, 220, 84, 211]),
tagLength: 128
}

export default class CryptoHelper {

private async buildKey(password: string) {
let utf8Encode = new TextEncoder();
let passwordBytes = utf8Encode.encode(password);

let passwordDigest = await crypto.subtle.digest({ name: 'SHA-256' }, passwordBytes);

let key = await crypto.subtle.importKey(
'raw',
passwordDigest,
algorithm,
false,
['encrypt', 'decrypt']
);

return key;
}

public async encryptToBase64(text: string, password: string): Promise<string> {
let key = await this.buildKey(password);

let utf8Encode = new TextEncoder();
let bytesToEncrypt = utf8Encode.encode(text);

// encrypt into bytes
let encryptedBytes = new Uint8Array(await crypto.subtle.encrypt(
algorithm, key, bytesToEncrypt
));

//convert array to base64
let base64Text = btoa(String.fromCharCode(...encryptedBytes));

return base64Text;
}

private stringToArray(str: string): Uint8Array {
var result = [];
for (var i = 0; i < str.length; i++) {
result.push(str.charCodeAt(i));
}
return new Uint8Array(result);
}

public async decryptFromBase64(base64Encoded: string, password: string): Promise<string> {
try {
// convert base 64 to array
let bytesToDecrypt = this.stringToArray(atob(base64Encoded));

let key = await this.buildKey(password);

// decrypt into bytes
let decryptedBytes = await crypto.subtle.decrypt(algorithm, key, bytesToDecrypt);

// convert bytes to text
let utf8Decode = new TextDecoder();
let decryptedText = utf8Decode.decode(decryptedBytes);
return decryptedText;
} catch (e) {
return null;
}
}

}
136 changes: 63 additions & 73 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,133 +1,123 @@
import { App, Modal, Notice, Plugin, MarkdownView } from 'obsidian';
import CryptoES from 'crypto-es';
import CryptoHelper from './CryptoHelper';


const _PREFIX:string = '%%🔐 ';
const _SUFFIX:string = ' 🔐%%';
const _PREFIX: string = '%%🔐 ';
const _SUFFIX: string = ' 🔐%%';

export default class MeldEncrypt extends Plugin {
async onload() {

async onload() {
this.addCommand({
id: 'encrypt-decrypt',
name: 'Encrypt/Decrypt',
checkCallback: this._processEncryptDecryptCommand.bind(this)
checkCallback: this.processEncryptDecryptCommand.bind(this)
});
}

_processEncryptDecryptCommand( checking: boolean ) : boolean {
processEncryptDecryptCommand(checking: boolean): boolean {

let mdview = this.app.workspace.getActiveViewOfType(MarkdownView);
const mdview = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!mdview) {
return false;
}

let editor = mdview.sourceMode.cmEditor;
if (!editor){
return false;
const editor = mdview.sourceMode.cmEditor;
if (!editor) {
return false;
}

var startPos = editor.listSelections().first().from();
var endPos = editor.listSelections().last().to();
var lastCharPos = editor.lineInfo(endPos.line).text.length;

// TODO: This causes the selection to expand, need to find a way
// to extract text without extending the selection.
editor.extendSelection(
{ line:startPos.line, ch:0 },
{ line:endPos.line, ch: lastCharPos }
);

const selectionText = editor.getSelection();

if ( selectionText.length == 0 ){

const startLine = editor.getCursor('from').line;
const startPos = { line: startLine, ch: 0 }; // want the start of the first line

const endLine = editor.getCursor('to').line;
const endLineText = editor.getLine(endLine);
const endPos = { line: endLine, ch: endLineText.length }; // want the end of last line

const selectionText = editor.getRange(startPos, endPos);

if (selectionText.length == 0) {
return false;
}

const decrypt = selectionText.startsWith(_PREFIX) && selectionText.endsWith(_SUFFIX);
const encrypt = !selectionText.contains(_PREFIX) && !selectionText.contains(_SUFFIX);
if ( !decrypt && !encrypt ){

if (!decrypt && !encrypt) {
return false;
}
if ( checking ){

if (checking) {
return true;
}


// Fetch password from user
const pwModal = new PasswordModal( this.app );
pwModal.onClose = () => {
if ( pwModal.password ){
const pwModal = new PasswordModal(this.app);
pwModal.onClose = async () => {
if (pwModal.password) {

// what should we do with it?
if ( decrypt ){
if (decrypt) {

// decrypt
const decryptedText = this._decrypt( selectionText, pwModal.password );
if (decryptedText === null){
const decryptedText = await this.decrypt(selectionText, pwModal.password);
if (decryptedText === null) {
new Notice('❌ Decryption failed!');
}else{
const textModal = new TextModal( this.app, '🔓' );
textModal.text = decryptedText;
textModal.onClose = () =>{
editor.focus();
}
} else {
const textModal = new TextModal(this.app, '🔓', decryptedText);
textModal.onClose = () => { editor.focus(); }
textModal.open();
}
}else if(encrypt){

} else if (encrypt) {

//encrypt
const encryptedText = this._encrypt( selectionText, pwModal.password );
editor.replaceSelection( encryptedText );
}else{
const encryptedText = await this.encrypt(selectionText, pwModal.password);
editor.setSelection(startPos, endPos);
editor.replaceSelection(encryptedText, 'around');

} else {
return false;
}
}

}
pwModal.open();

return true;
}



private _encrypt( text: string, password: string): string {
return _PREFIX + CryptoES.AES.encrypt(text, password).toString() + _SUFFIX;
private async encrypt(text: string, password: string): Promise<string> {
const ch = new CryptoHelper();
return _PREFIX + await ch.encryptToBase64(text, password) + _SUFFIX;
}

private _decrypt( text: string, password: string ):string {
try{
const ciphertext = text.replace(_PREFIX, '').replace(_SUFFIX, '');
const bytes = CryptoES.AES.decrypt(ciphertext, password);
if ( bytes.sigBytes > 0 ){
return bytes.toString(CryptoES.enc.Utf8);
}
return null; // decrypt failed
}catch{
return null; // decrypt failed
}
private async decrypt(text: string, password: string): Promise<string> {
const ciphertext = text.replace(_PREFIX, '').replace(_SUFFIX, '');
const ch = new CryptoHelper();
return await ch.decryptFromBase64(ciphertext, password);
}

}

class PasswordModal extends Modal {
password: string = null;
constructor( app: App ) {

constructor(app: App) {
super(app);
}

onOpen() {
let {contentEl} = this;
let { contentEl } = this;

const pwInputEl = contentEl.createDiv().createEl('input');
pwInputEl.type = 'password';
pwInputEl.placeholder = '🔑 Enter your password';
pwInputEl.style.width = '100%';
pwInputEl.focus();

pwInputEl.addEventListener( 'keyup', (ev) => {
if ( ev.code === 'Enter' && pwInputEl.value.length > 0 ) {
pwInputEl.addEventListener('keyup', (ev) => {
if (ev.code === 'Enter' && pwInputEl.value.length > 0) {
ev.preventDefault();
this.password = pwInputEl.value;
this.close();
Expand All @@ -141,24 +131,24 @@ class PasswordModal extends Modal {
class TextModal extends Modal {
text: string;

constructor(app: App, title: string, text: string ='') {
constructor(app: App, title: string, text: string = '') {
super(app);
this.text = text;
this.titleEl.innerText = title;
}

onOpen() {
let {contentEl} = this;
let { contentEl } = this;

const textEl = contentEl.createEl('textarea');
textEl.value = this.text;
textEl.style.width = '100%';
textEl.style.height = '100%';
textEl.rows = 10;
textEl.readOnly = true;
//textEl.focus(); // Doesn't seem to work here...
setImmediate(() =>{textEl.focus()}); //... but this does
setImmediate(() => { textEl.focus() }); //... but this does

}

}

0 comments on commit 5cb2b7d

Please sign in to comment.