Browse Source

Fix Permanent IV / IV reuse #11

pull/16/head 1.4.0
Cleon Pinto 4 years ago
parent
commit
f784e6dc69
  1. 5
      README.md
  2. 33
      docs/crypto-details.md
  3. 4
      manifest.json
  4. 2
      package.json
  5. 7
      rollup.config.js
  6. 104
      src/CryptoHelper.ts
  7. 2
      src/DecryptModal.ts
  8. 114
      src/main.ts
  9. 15
      test-vault/Example.md
  10. 1
      versions.json

5
README.md

@ -10,7 +10,7 @@ Under the hood it uses the Advanced Encryption Standard (AES) in GCM mode. @@ -10,7 +10,7 @@ Under the hood it uses the Advanced Encryption Standard (AES) in GCM mode.
> WARNING: Use at your own risk.
> - Your passwords are never stored anywhere, if you forget your password you can't decrypt your text.
> - There havn't been any audits for the soundness of encryption methods being used. Unwanted decyption by a 3rd party may still be possible if they have access to your files.
> - There haven't been any audits for the soundness of encryption methods being used. Unwanted decryption by a 3rd party may still be possible if they have access to your files.
## Usage
@ -65,6 +65,3 @@ You can install the plugin via the Community Plugins tab within Obsidian by sear @@ -65,6 +65,3 @@ You can install the plugin via the Community Plugins tab within Obsidian by sear
<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=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff"></a>
Thank you for your support 🙏
## More information
- [Encryption details](https://github.com/meld-cp/obsidian-encrypt/blob/main/docs/crypto-details.md)

33
docs/crypto-details.md

@ -1,33 +0,0 @@ @@ -1,33 +0,0 @@
# 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 Base64 encoded and surrounded by markers 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. From 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/decrypt
const key = await crypto.subtle.importKey(
'raw',
await crypto.subtle.digest({ name: 'SHA-256' }, new TextEncoder().encode(password)),
algorithm,
false,
['encrypt', 'decrypt']
);
```

4
manifest.json

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
{
"id": "meld-encrypt",
"name": "Meld Encrypt",
"version": "1.3.4",
"minAppVersion": "0.12.12",
"version": "1.4.0",
"minAppVersion": "0.12.15",
"description": "Hide secrets in your notes",
"author": "meld-cp",
"authorUrl": "https://github.com/meld-cp/obsidian-encrypt",

2
package.json

@ -18,7 +18,7 @@ @@ -18,7 +18,7 @@
"obsidian": "https://github.com/obsidianmd/obsidian-api.git",
"rollup": "^2.32.1",
"rollup-plugin-copy": "^3.4.0",
"tslib": "^2.0.3",
"tslib": "^2.3.1",
"typescript": "^4.0.3"
}
}

7
rollup.config.js

@ -29,14 +29,7 @@ export default { @@ -29,14 +29,7 @@ export default {
src: 'dist/*',
dest: 'test-vault/.obsidian/plugins/meld-encrypt/'
},
// { src: 'assets/images/**/*', dest: 'dist/public/images' }
]
// assets: [
// 'dest/main.js',
// 'manifest.json',
// 'src/style.css',
// ],
// outputDir: 'test-vault/.obsidian/plugins/meld-encrypt'
})
],

104
src/CryptoHelper.ts

@ -1,11 +1,105 @@ @@ -1,11 +1,105 @@
const vectorSize = 16;
const utf8Encoder = new TextEncoder();
const utf8Decoder = new TextDecoder();
const iterations = 1000;
const salt = utf8Encoder.encode(navigator.userAgent);
export class CryptoHelperV2 {
private async deriveKey(password:string) :Promise<CryptoKey> {
const buffer = utf8Encoder.encode(password);
const key = await 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;
}
public async encryptToBase64(text: string, password: string): Promise<string> {
const key = await this.deriveKey(password);
const textBytesToEncrypt = utf8Encoder.encode(text);
const vector = crypto.getRandomValues(new Uint8Array(vectorSize));
// encrypt into bytes
const encryptedBytes = new Uint8Array(
await 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;
}
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 {
let bytesToDecode = this.stringToArray(atob(base64Encoded));
// 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 algorithm = {
const algorithmObsolete = {
name: 'AES-GCM',
iv: new Uint8Array([196, 190, 240, 190, 188, 78, 41, 132, 15, 220, 84, 211]),
tagLength: 128
}
export default class CryptoHelper {
export class CryptoHelperObsolete {
private async buildKey(password: string) {
let utf8Encode = new TextEncoder();
@ -16,7 +110,7 @@ export default class CryptoHelper { @@ -16,7 +110,7 @@ export default class CryptoHelper {
let key = await crypto.subtle.importKey(
'raw',
passwordDigest,
algorithm,
algorithmObsolete,
false,
['encrypt', 'decrypt']
);
@ -32,7 +126,7 @@ export default class CryptoHelper { @@ -32,7 +126,7 @@ export default class CryptoHelper {
// encrypt into bytes
let encryptedBytes = new Uint8Array(await crypto.subtle.encrypt(
algorithm, key, bytesToEncrypt
algorithmObsolete, key, bytesToEncrypt
));
//convert array to base64
@ -57,7 +151,7 @@ export default class CryptoHelper { @@ -57,7 +151,7 @@ export default class CryptoHelper {
let key = await this.buildKey(password);
// decrypt into bytes
let decryptedBytes = await crypto.subtle.decrypt(algorithm, key, bytesToDecrypt);
let decryptedBytes = await crypto.subtle.decrypt(algorithmObsolete, key, bytesToDecrypt);
// convert bytes to text
let utf8Decode = new TextDecoder();

2
src/DecryptModal.ts

@ -19,7 +19,7 @@ export default class DecryptModal extends Modal { @@ -19,7 +19,7 @@ export default class DecryptModal extends Modal {
textEl.rows = 10;
textEl.readOnly = true;
//textEl.focus(); // Doesn't seem to work here...
setImmediate(() => { textEl.focus() }); //... but this does
setTimeout(() => { textEl.focus() },100); //... but this does
const btnContainerEl = contentEl.createDiv('');

114
src/main.ts

@ -1,10 +1,11 @@ @@ -1,10 +1,11 @@
import { Notice, Plugin, MarkdownView } from 'obsidian';
import { Notice, Plugin, MarkdownView, Editor } from 'obsidian';
import DecryptModal from './DecryptModal';
import PasswordModal from './PasswordModal';
import CryptoHelper from './CryptoHelper';
import { CryptoHelperV2, CryptoHelperObsolete} from './CryptoHelper';
import MeldEncryptSettingsTab from './MeldEncryptSettingsTab';
const _PREFIX: string = '%%🔐 ';
const _PREFIX_OBSOLETE: string = '%%🔐 ';
const _PREFIX_A: string = '%%🔐α ';
const _SUFFIX: string = ' 🔐%%';
interface MeldEncryptPluginSettings {
@ -59,7 +60,7 @@ export default class MeldEncrypt extends Plugin { @@ -59,7 +60,7 @@ export default class MeldEncrypt extends Plugin {
return false;
}
const editor = mdview.sourceMode.cmEditor;
const editor = mdview.editor;
if (!editor) {
return false;
}
@ -77,8 +78,11 @@ export default class MeldEncrypt extends Plugin { @@ -77,8 +78,11 @@ export default class MeldEncrypt extends Plugin {
return false;
}
const decrypt = selectionText.startsWith(_PREFIX) && selectionText.endsWith(_SUFFIX);
const encrypt = !selectionText.contains(_PREFIX) && !selectionText.contains(_SUFFIX);
const decrypt_obs = selectionText.startsWith(_PREFIX_OBSOLETE) && selectionText.endsWith(_SUFFIX);
const decrypt_a = selectionText.startsWith(_PREFIX_A) && selectionText.endsWith(_SUFFIX);
const decrypt = decrypt_obs || decrypt_a;
const encrypt = !selectionText.contains(_PREFIX_OBSOLETE) && !selectionText.contains(_SUFFIX);
if (!decrypt && !encrypt) {
return false;
@ -99,12 +103,13 @@ export default class MeldEncrypt extends Plugin { @@ -99,12 +103,13 @@ export default class MeldEncrypt extends Plugin {
)
;
if (isRememberPasswordExpired) {
const confirmPassword = encrypt && this.settings.confirmPassword;
if ( isRememberPasswordExpired || confirmPassword ) {
// forget password
this.passwordLastUsed = '';
}
const confirmPassword = encrypt && this.settings.confirmPassword;
const pwModal = new PasswordModal(this.app, confirmPassword, this.passwordLastUsed);
pwModal.onClose = () => {
const pw = pwModal.password ?? ''
@ -131,14 +136,26 @@ export default class MeldEncrypt extends Plugin { @@ -131,14 +136,26 @@ export default class MeldEncrypt extends Plugin {
endPos
);
} else {
this.decryptSelection(
editor,
selectionText,
pw,
startPos,
endPos,
decryptInPlace
);
if (decrypt_a){
this.decryptSelection_a(
editor,
selectionText,
pw,
startPos,
endPos,
decryptInPlace
);
}else{
this.decryptSelectionObsolete(
editor,
selectionText,
pw,
startPos,
endPos,
decryptInPlace
);
}
}
}
pwModal.open();
@ -147,30 +164,66 @@ export default class MeldEncrypt extends Plugin { @@ -147,30 +164,66 @@ export default class MeldEncrypt extends Plugin {
}
private async encryptSelection(
editor: CodeMirror.Editor,
editor: Editor,
selectionText: string,
password: string,
finalSelectionStart: CodeMirror.Position,
finalSelectionEnd: CodeMirror.Position,
) {
//encrypt
const crypto = new CryptoHelper();
const crypto = new CryptoHelperV2();
const base64EncryptedText = this.addMarkers(await crypto.encryptToBase64(selectionText, password));
editor.setSelection(finalSelectionStart, finalSelectionEnd);
editor.replaceSelection(base64EncryptedText, 'around');
editor.replaceSelection(base64EncryptedText);
}
private async decryptSelection_a(
editor: Editor,
selectionText: string,
password: string,
selectionStart: CodeMirror.Position,
selectionEnd: CodeMirror.Position,
decryptInPlace: boolean
) {
//console.log('decryptSelection_a');
// decrypt
const base64CipherText = this.removeMarkers(selectionText);
const crypto = new CryptoHelperV2();
const decryptedText = await crypto.decryptFromBase64(base64CipherText, password);
if (decryptedText === null) {
new Notice('❌ Decryption failed!');
} else {
if (decryptInPlace) {
editor.setSelection(selectionStart, selectionEnd);
editor.replaceSelection(decryptedText);
} else {
const decryptModal = new DecryptModal(this.app, '🔓', decryptedText);
decryptModal.onClose = () => {
editor.focus();
if (decryptModal.decryptInPlace) {
editor.setSelection(selectionStart, selectionEnd);
editor.replaceSelection(decryptedText);
}
}
decryptModal.open();
}
}
}
private async decryptSelection(
editor: CodeMirror.Editor,
private async decryptSelectionObsolete(
editor: Editor,
selectionText: string,
password: string,
selectionStart: CodeMirror.Position,
selectionEnd: CodeMirror.Position,
decryptInPlace: boolean
) {
//console.log('decryptSelectionObsolete');
// decrypt
const base64CipherText = this.removeMarkers(selectionText);
const crypto = new CryptoHelper();
const crypto = new CryptoHelperObsolete();
const decryptedText = await crypto.decryptFromBase64(base64CipherText, password);
if (decryptedText === null) {
new Notice('❌ Decryption failed!');
@ -178,14 +231,14 @@ export default class MeldEncrypt extends Plugin { @@ -178,14 +231,14 @@ export default class MeldEncrypt extends Plugin {
if (decryptInPlace) {
editor.setSelection(selectionStart, selectionEnd);
editor.replaceSelection(decryptedText, 'around');
editor.replaceSelection(decryptedText);
} else {
const decryptModal = new DecryptModal(this.app, '🔓', decryptedText);
decryptModal.onClose = () => {
editor.focus();
if (decryptModal.decryptInPlace) {
editor.setSelection(selectionStart, selectionEnd);
editor.replaceSelection(decryptedText, 'around');
editor.replaceSelection(decryptedText);
}
}
decryptModal.open();
@ -194,15 +247,18 @@ export default class MeldEncrypt extends Plugin { @@ -194,15 +247,18 @@ export default class MeldEncrypt extends Plugin {
}
private removeMarkers(text: string): string {
if (text.startsWith(_PREFIX) && text.endsWith(_SUFFIX)) {
return text.replace(_PREFIX, '').replace(_SUFFIX, '');
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;
}
private addMarkers(text: string): string {
if (!text.contains(_PREFIX) && !text.contains(_SUFFIX)) {
return _PREFIX.concat(text, _SUFFIX);
if (!text.contains(_PREFIX_OBSOLETE) && !text.contains(_PREFIX_A) && !text.contains(_SUFFIX)) {
return _PREFIX_A.concat(text, _SUFFIX);
}
return text;
}

15
test-vault/Example.md

@ -1,8 +1,9 @@ @@ -1,8 +1,9 @@
%%🔐 U2FsdGVkX1/FhaHNpE+CxTB7p9TFPvtvyjf791wgo9XlCPAG8s7/AS0W9XJI3ou+IY37A3Wj1xQ4Oc0O/oFVz3yfe0quKsHA8MQXJRL3GpPeXgPlttesCBBOj1q+tQvr 🔐%%
%%🔐 U2FsdGVkX187VZNelzwPK+Z3GVHEPcgfd1CKmpSC644= 🔐%%
# v1
pw:123
%%🔐 jivo/4M89jau2ynN9PX3dfKlfwc= 🔐%%
# v2
pw:123
%%🔐α Rs3x0vo8Z+owqbgh9jYesxNI3MjNII7VS+ixswUdChg9/ooe 🔐%%
%%🔐α vdv2q+uXrr1CVYHrN00vvmqUPlLtaDnsb37h5lG82gwyG05k 🔐%%

1
versions.json

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
{
"1.4.0": "0.12.15",
"1.3.4": "0.12.12",
"1.3.3": "0.11.3",
"1.3.2": "0.11.3",

Loading…
Cancel
Save