/*** |Name || |Description|Adds AES encryption and password protection and several macros to work with them| |Version |1.7.2| |Source |https://github.com/YakovL/TiddlyWiki_EncryptionPlugin/blob/master/EncryptionPlugin.js| |Author |Yakov Litvin| |Forked from|[[EncryptedVaultPlugin|http://visualtw.ouvaton.org/VisualTW.html#EncryptedVaultPlugin]] by Pascal Collin (now available at [[GitHub|https://yakovl.github.io/VisualTW2/VisualTW2.html#EncryptedVaultPlugin]] or [[in web archive|https://web.archive.org/web/20160130130224/http://visualtw.ouvaton.org/VisualTW.html#EncryptedVaultPlugin]])| |''Browser:''|plugin is supposed to work in any modern browser, but it is recommended to create a backup of your TW before trying it with your setup| patched plugin, made a simplest test; fixed a couple of bugs (how ~EncryptedVaultPlugin even worked previously?), moved styles from custom block to a shadow, removed several overridings with decorators, changed UI for pass (hidden, autofocus, enter to apply); now {{PoG{test thoroughly (migration from ~EncryptedVaultPlugin; including, DefaultTiddlers, first run of the plugin, macros appearence and usage, data format compatibility, including unencrypted, ...; .oO MVP for ...) and update metadata}}}: |''License:''|[[BSD open source license|License]]| !Description * Create an ''encrypted vault'' where all tiddlers are ''password protected''. * By default, only the system tiddlers (shadow ones and those tagged {{{systemConfig}}}) aren't encrypted. Also, if you create [[ListUnencrypted]] and put a [[filter|classic.tiddlywiki.com#Filters]] there, the filtered tiddlers won't be encrypted, too. * Even shadow tiddlers (MainMenu, SiteTitle, PageTemplate, StyleSheet, ...) ''can be encrypted''. The shadow version is used until unlocking. !Demo --Use <> button on a protected wiki. By example: http://visualtw.ouvaton.org/demo/EncryptedVaultPlugin.html-- !Installation # Import/copy the plugin (tagged with {{{systemConfig}}}), save and reload (as usual) # Save one more time (to create the encrypted vault) and reload # Set a password !Usage * TODO: describe backward compatibility with .. plugin ("migration") * Use <><> button (available by default in SideBarOptions) * Use a blank password to save unencrypted (disable vault usage) * Use {{{unencrypted}}} tag to avoid encryption for some tiddler * Use {{{forceEncryption}}} tag to force some shadow tiddler to be encrypted * TODO: describe other macros !Configuration The following macros are available: * {{{<>}}} creates a button to unlock the encrypted vault (all parameters are optional) * {{{<>}}} if unlocked, creates a button to set the current password (all parameters are optional) * {{{<>}}} if locked, creates a button to purge a locked vault, useful for lost password (encrypted content is the deleted) * {{{<>}}} displays tiddlyText (wikified) if the vault is locked * {{{<>}}} displays tiddlyText (wikified) if the vault is unlocked <><> <><> ***/ /*** Stanford Javascript Crypto Library {{DDnc{v.1.0.6 ([[63eed5|https://github.com/bitwiseshiftleft/sjcl/commit/63eed58b9dc395afb3c03df8d70d7e7bf4c88b1b]]), update!}}}, source: https://github.com/bitwiseshiftleft/sjcl + one line to make `sjcl` globally accessible ***/ //{{{ "use strict";var sjcl={cipher:{},hash:{},keyexchange:{},mode:{},misc:{},codec:{},exception:{corrupt:function(a){this.toString=function(){return"CORRUPT: "+this.message};this.message=a},invalid:function(a){this.toString=function(){return"INVALID: "+this.message};this.message=a},bug:function(a){this.toString=function(){return"BUG: "+this.message};this.message=a},notReady:function(a){this.toString=function(){return"NOT READY: "+this.message};this.message=a}}}; sjcl.cipher.aes=function(a){this.s[0][0][0]||this.O();var b,c,d,e,f=this.s[0][4],g=this.s[1];b=a.length;var h=1;if(4!==b&&6!==b&&8!==b)throw new sjcl.exception.invalid("invalid aes key size");this.b=[d=a.slice(0),e=[]];for(a=b;a<4*b+28;a++){c=d[a-1];if(0===a%b||8===b&&4===a%b)c=f[c>>>24]<<24^f[c>>16&255]<<16^f[c>>8&255]<<8^f[c&255],0===a%b&&(c=c<<8^c>>>24^h<<24,h=h<<1^283*(h>>7));d[a]=d[a-b]^c}for(b=0;a;b++,a--)c=d[b&3?a:a-4],e[b]=4>=a||4>b?c:g[0][f[c>>>24]]^g[1][f[c>>16&255]]^g[2][f[c>>8&255]]^g[3][f[c& 255]]}; sjcl.cipher.aes.prototype={encrypt:function(a){return t(this,a,0)},decrypt:function(a){return t(this,a,1)},s:[[[],[],[],[],[]],[[],[],[],[],[]]],O:function(){var a=this.s[0],b=this.s[1],c=a[4],d=b[4],e,f,g,h=[],k=[],l,n,m,p;for(e=0;0x100>e;e++)k[(h[e]=e<<1^283*(e>>7))^e]=e;for(f=g=0;!c[f];f^=l||1,g=k[g]||1)for(m=g^g<<1^g<<2^g<<3^g<<4,m=m>>8^m&255^99,c[f]=m,d[m]=f,n=h[e=h[l=h[f]]],p=0x1010101*n^0x10001*e^0x101*l^0x1010100*f,n=0x101*h[m]^0x1010100*m,e=0;4>e;e++)a[e][f]=n=n<<24^n>>>8,b[e][m]=p=p<<24^p>>>8;for(e= 0;5>e;e++)a[e]=a[e].slice(0),b[e]=b[e].slice(0)}}; function t(a,b,c){if(4!==b.length)throw new sjcl.exception.invalid("invalid aes block size");var d=a.b[c],e=b[0]^d[0],f=b[c?3:1]^d[1],g=b[2]^d[2];b=b[c?1:3]^d[3];var h,k,l,n=d.length/4-2,m,p=4,r=[0,0,0,0];h=a.s[c];a=h[0];var q=h[1],v=h[2],w=h[3],x=h[4];for(m=0;m>>24]^q[f>>16&255]^v[g>>8&255]^w[b&255]^d[p],k=a[f>>>24]^q[g>>16&255]^v[b>>8&255]^w[e&255]^d[p+1],l=a[g>>>24]^q[b>>16&255]^v[e>>8&255]^w[f&255]^d[p+2],b=a[b>>>24]^q[e>>16&255]^v[f>>8&255]^w[g&255]^d[p+3],p+=4,e=h,f=k,g=l;for(m= 0;4>m;m++)r[c?3&-m:m]=x[e>>>24]<<24^x[f>>16&255]<<16^x[g>>8&255]<<8^x[b&255]^d[p++],h=e,e=f,f=g,g=b,b=h;return r} sjcl.bitArray={bitSlice:function(a,b,c){a=sjcl.bitArray.$(a.slice(b/32),32-(b&31)).slice(1);return void 0===c?a:sjcl.bitArray.clamp(a,c-b)},extract:function(a,b,c){var d=Math.floor(-b-c&31);return((b+c-1^b)&-32?a[b/32|0]<<32-d^a[b/32+1|0]>>>d:a[b/32|0]>>>d)&(1<>b-1,1));return a},partial:function(a,b,c){return 32===a?b:(c?b|0:b<<32-a)+0x10000000000*a},getPartial:function(a){return Math.round(a/0x10000000000)||32},equal:function(a,b){if(sjcl.bitArray.bitLength(a)!==sjcl.bitArray.bitLength(b))return!1;var c=0,d;for(d=0;d>>b),c=a[e]<<32-b;e=a.length?a[a.length-1]:0;a=sjcl.bitArray.getPartial(e);d.push(sjcl.bitArray.partial(b+a&31,32>>24|c>>>8&0xff00|(c&0xff00)<<8|c<<24;return a}}; sjcl.codec.utf8String={fromBits:function(a){var b="",c=sjcl.bitArray.bitLength(a),d,e;for(d=0;d>>24),e<<=8;return decodeURIComponent(escape(b))},toBits:function(a){a=unescape(encodeURIComponent(a));var b=[],c,d=0;for(c=0;c>>g)>>>e),gn){if(!b)try{return sjcl.codec.base32hex.toBits(a)}catch(p){}throw new sjcl.exception.invalid("this isn't "+m+"!");}h>e?(h-=e,f.push(l^n>>>h),l=n<>>e)>>>26),6>e?(g=a[c]<<6-e,e+=26,c++):(g<<=6,e-=6);for(;d.length&3&&!b;)d+="=";return d},toBits:function(a,b){a=a.replace(/\s|=/g,"");var c=[],d,e=0,f=sjcl.codec.base64.B,g=0,h;b&&(f=f.substr(0,62)+"-_");for(d=0;dh)throw new sjcl.exception.invalid("this isn't base64!");26>>e),g=h<<32-e):(e+=6,g^=h<<32-e)}e&56&&c.push(sjcl.bitArray.partial(e&56,g,1));return c}};sjcl.codec.base64url={fromBits:function(a){return sjcl.codec.base64.fromBits(a,1,1)},toBits:function(a){return sjcl.codec.base64.toBits(a,1)}};sjcl.hash.sha256=function(a){this.b[0]||this.O();a?(this.F=a.F.slice(0),this.A=a.A.slice(0),this.l=a.l):this.reset()};sjcl.hash.sha256.hash=function(a){return(new sjcl.hash.sha256).update(a).finalize()}; sjcl.hash.sha256.prototype={blockSize:512,reset:function(){this.F=this.Y.slice(0);this.A=[];this.l=0;return this},update:function(a){"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));var b,c=this.A=sjcl.bitArray.concat(this.A,a);b=this.l;a=this.l=b+sjcl.bitArray.bitLength(a);if(0x1fffffffffffffb;c++){e=!0;for(d=2;d*d<=c;d++)if(0===c%d){e= !1;break}e&&(8>b&&(this.Y[b]=a(Math.pow(c,.5))),this.b[b]=a(Math.pow(c,1/3)),b++)}}}; function u(a,b){var c,d,e,f=a.F,g=a.b,h=f[0],k=f[1],l=f[2],n=f[3],m=f[4],p=f[5],r=f[6],q=f[7];for(c=0;64>c;c++)16>c?d=b[c]:(d=b[c+1&15],e=b[c+14&15],d=b[c&15]=(d>>>7^d>>>18^d>>>3^d<<25^d<<14)+(e>>>17^e>>>19^e>>>10^e<<15^e<<13)+b[c&15]+b[c+9&15]|0),d=d+q+(m>>>6^m>>>11^m>>>25^m<<26^m<<21^m<<7)+(r^m&(p^r))+g[c],q=r,r=p,p=m,m=n+d|0,n=l,l=k,k=h,h=d+(k&l^n&(k^l))+(k>>>2^k>>>13^k>>>22^k<<30^k<<19^k<<10)|0;f[0]=f[0]+h|0;f[1]=f[1]+k|0;f[2]=f[2]+l|0;f[3]=f[3]+n|0;f[4]=f[4]+m|0;f[5]=f[5]+p|0;f[6]=f[6]+r|0;f[7]= f[7]+q|0} sjcl.mode.ccm={name:"ccm",G:[],listenProgress:function(a){sjcl.mode.ccm.G.push(a)},unListenProgress:function(a){a=sjcl.mode.ccm.G.indexOf(a);-1k)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(f=2;4>f&&l>>>8*f;f++);f<15-k&&(f=15-k);c=h.clamp(c, 8*(15-f));b=sjcl.mode.ccm.V(a,b,c,d,e,f);g=sjcl.mode.ccm.C(a,g,c,b,e,f);return h.concat(g.data,g.tag)},decrypt:function(a,b,c,d,e){e=e||64;d=d||[];var f=sjcl.bitArray,g=f.bitLength(c)/8,h=f.bitLength(b),k=f.clamp(b,h-e),l=f.bitSlice(b,h-e),h=(h-e)/8;if(7>g)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(b=2;4>b&&h>>>8*b;b++);b<15-g&&(b=15-g);c=f.clamp(c,8*(15-b));k=sjcl.mode.ccm.C(a,k,c,l,e,b);a=sjcl.mode.ccm.V(a,k.data,c,d,e,b);if(!f.equal(k.tag,a))throw new sjcl.exception.corrupt("ccm: tag doesn't match"); return k.data},na:function(a,b,c,d,e,f){var g=[],h=sjcl.bitArray,k=h.i;d=[h.partial(8,(b.length?64:0)|d-2<<2|f-1)];d=h.concat(d,c);d[3]|=e;d=a.encrypt(d);if(b.length)for(c=h.bitLength(b)/8,65279>=c?g=[h.partial(16,c)]:0xffffffff>=c&&(g=h.concat([h.partial(16,65534)],[c])),g=h.concat(g,b),b=0;be||16n&&(sjcl.mode.ccm.fa(g/ k),n+=m),c[3]++,e=a.encrypt(c),b[g]^=e[0],b[g+1]^=e[1],b[g+2]^=e[2],b[g+3]^=e[3];return{tag:d,data:h.clamp(b,l)}}}; sjcl.mode.ocb2={name:"ocb2",encrypt:function(a,b,c,d,e,f){if(128!==sjcl.bitArray.bitLength(c))throw new sjcl.exception.invalid("ocb iv must be 128 bits");var g,h=sjcl.mode.ocb2.S,k=sjcl.bitArray,l=k.i,n=[0,0,0,0];c=h(a.encrypt(c));var m,p=[];d=d||[];e=e||64;for(g=0;g+4e.bitLength(c)&&(h=f(h,d(h)),c=e.concat(c,[-2147483648,0,0,0]));g=f(g,c); return a.encrypt(f(d(f(h,d(h))),g))},S:function(a){return[a[0]<<1^a[1]>>>31,a[1]<<1^a[2]>>>31,a[2]<<1^a[3]>>>31,a[3]<<1^135*(a[0]>>>31)]}}; sjcl.mode.gcm={name:"gcm",encrypt:function(a,b,c,d,e){var f=b.slice(0);b=sjcl.bitArray;d=d||[];a=sjcl.mode.gcm.C(!0,a,f,d,c,e||128);return b.concat(a.data,a.tag)},decrypt:function(a,b,c,d,e){var f=b.slice(0),g=sjcl.bitArray,h=g.bitLength(f);e=e||128;d=d||[];e<=h?(b=g.bitSlice(f,h-e),f=g.bitSlice(f,0,h-e)):(b=f,f=[]);a=sjcl.mode.gcm.C(!1,a,f,d,c,e);if(!g.equal(a.tag,b))throw new sjcl.exception.corrupt("gcm: tag doesn't match");return a.data},ka:function(a,b){var c,d,e,f,g,h=sjcl.bitArray.i;e=[0,0, 0,0];f=b.slice(0);for(c=0;128>c;c++){(d=0!==(a[Math.floor(c/32)]&1<<31-c%32))&&(e=h(e,f));g=0!==(f[3]&1);for(d=3;0>>1|(f[d-1]&1)<<31;f[0]>>>=1;g&&(f[0]^=-0x1f000000)}return e},j:function(a,b,c){var d,e=c.length;b=b.slice(0);for(d=0;de&&(a=b.hash(a));for(d=0;dd||0>c)throw new sjcl.exception.invalid("invalid params to pbkdf2");"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));"string"===typeof b&&(b=sjcl.codec.utf8String.toBits(b));e=e||sjcl.misc.hmac;a=new e(a);var f,g,h,k,l=[],n=sjcl.bitArray;for(k=1;32*l.length<(d||1);k++){e=f=a.encrypt(n.concat(b,[k]));for(g=1;gg;g++)e.push(0x100000000*Math.random()|0);for(g=0;g=1<this.o&&(this.o= f);this.P++;this.b=sjcl.hash.sha256.hash(this.b.concat(e));this.L=new sjcl.cipher.aes(this.b);for(d=0;4>d&&(this.h[d]=this.h[d]+1|0,!this.h[d]);d++);}for(d=0;d>>1;this.c[g].update([d,this.N++,2,b,f,a.length].concat(a))}break;case "string":void 0===b&&(b=a.length);this.c[g].update([d,this.N++,3,b,f,a.length]);this.c[g].update(a);break;default:k=1}if(k)throw new sjcl.exception.bug("random: addEntropy only supports number, array of numbers or string");this.m[g]+=b;this.f+=b;h===this.u&&(this.isReady()!==this.u&&A("seeded",Math.max(this.o,this.f)),A("progress",this.getProgress()))}, isReady:function(a){a=this.T[void 0!==a?a:this.M];return this.o&&this.o>=a?this.m[0]>this.ba&&(new Date).valueOf()>this.Z?this.J|this.I:this.I:this.f>=a?this.J|this.u:this.u},getProgress:function(a){a=this.T[a?a:this.M];return this.o>=a?1:this.f>a?1:this.f/a},startCollectors:function(){if(!this.D){this.a={loadTimeCollector:B(this,this.ma),mouseCollector:B(this,this.oa),keyboardCollector:B(this,this.la),accelerometerCollector:B(this,this.ea),touchCollector:B(this,this.qa)};if(window.addEventListener)window.addEventListener("load", this.a.loadTimeCollector,!1),window.addEventListener("mousemove",this.a.mouseCollector,!1),window.addEventListener("keypress",this.a.keyboardCollector,!1),window.addEventListener("devicemotion",this.a.accelerometerCollector,!1),window.addEventListener("touchmove",this.a.touchCollector,!1);else if(document.attachEvent)document.attachEvent("onload",this.a.loadTimeCollector),document.attachEvent("onmousemove",this.a.mouseCollector),document.attachEvent("keypress",this.a.keyboardCollector);else throw new sjcl.exception.bug("can't attach event"); this.D=!0}},stopCollectors:function(){this.D&&(window.removeEventListener?(window.removeEventListener("load",this.a.loadTimeCollector,!1),window.removeEventListener("mousemove",this.a.mouseCollector,!1),window.removeEventListener("keypress",this.a.keyboardCollector,!1),window.removeEventListener("devicemotion",this.a.accelerometerCollector,!1),window.removeEventListener("touchmove",this.a.touchCollector,!1)):document.detachEvent&&(document.detachEvent("onload",this.a.loadTimeCollector),document.detachEvent("onmousemove", this.a.mouseCollector),document.detachEvent("keypress",this.a.keyboardCollector)),this.D=!1)},addEventListener:function(a,b){this.K[a][this.ga++]=b},removeEventListener:function(a,b){var c,d,e=this.K[a],f=[];for(d in e)e.hasOwnProperty(d)&&e[d]===b&&f.push(d);for(c=0;cb&&(a.h[b]=a.h[b]+1|0,!a.h[b]);b++);return a.L.encrypt(a.h)} function B(a,b){return function(){b.apply(a,arguments)}}sjcl.random=new sjcl.prng(6); a:try{var D,E,F,G;if(G="undefined"!==typeof module&&module.exports){var H;try{H=require("crypto")}catch(a){H=null}G=E=H}if(G&&E.randomBytes)D=E.randomBytes(128),D=new Uint32Array((new Uint8Array(D)).buffer),sjcl.random.addEntropy(D,1024,"crypto['randomBytes']");else if("undefined"!==typeof window&&"undefined"!==typeof Uint32Array){F=new Uint32Array(32);if(window.crypto&&window.crypto.getRandomValues)window.crypto.getRandomValues(F);else if(window.msCrypto&&window.msCrypto.getRandomValues)window.msCrypto.getRandomValues(F); else break a;sjcl.random.addEntropy(F,1024,"crypto['getRandomValues']")}}catch(a){"undefined"!==typeof window&&window.console&&(console.log("There was an error collecting entropy from the browser:"),console.log(a))} sjcl.json={defaults:{v:1,iter:1E4,ks:128,ts:64,mode:"ccm",adata:"",cipher:"aes"},ja:function(a,b,c,d){c=c||{};d=d||{};var e=sjcl.json,f=e.g({iv:sjcl.random.randomWords(4,0)},e.defaults),g;e.g(f,c);c=f.adata;"string"===typeof f.salt&&(f.salt=sjcl.codec.base64.toBits(f.salt));"string"===typeof f.iv&&(f.iv=sjcl.codec.base64.toBits(f.iv));if(!sjcl.mode[f.mode]||!sjcl.cipher[f.cipher]||"string"===typeof a&&100>=f.iter||64!==f.ts&&96!==f.ts&&128!==f.ts||128!==f.ks&&192!==f.ks&&0x100!==f.ks||2>f.iv.length|| 4=b.iter||64!==b.ts&&96!==b.ts&&128!==b.ts||128!==b.ks&&192!==b.ks&&0x100!==b.ks||!b.iv||2>b.iv.length||4>/, "<><><>"); config.shadowTiddlers.GettingStarted += "\n\n<><>" + "<><>"; merge(config.messages, { vaultCreationInfo: "The encrypted vault has been created", passwordSet: "🔒 Successfully set password", passwordUnset: "🔓 Successfully unset password", purgeConfirm: "Purge the encrypted vault ?\n\nAll unlocked content will be lost.", vaultPurgedInfo: "All contents have been purged from encrypted vault.\nPassword has been blanked.\nYou must save once to apply this changes.", vaultEncryptedInfo: "Saving with encryption", vaultUnchangedInfo: "No changes in Encrypted vault", noLockedVaultNoPurge: "No locked encrypted vault, nothing to purge.", emptyVaultInfo: "Saving without encryption", saveWithLockedVaultConfirm: "Encrypted vault is locked. No changes will apply inside.\n\nAre you sure ?", confirmOverload: "This following tiddler already exists in system store. Overload ?\nOK : the encrypted version will replace the system store version\nCancel : the system store version will replace the encrypted version" }); config.extensions = config.extensions || {} config.extensions.vault = { // TODO: encapsulate sjcl from global context? callSjcl: function(method, inputText, password) { if(!password) return try { var outputText = window.sjcl[method](password, inputText) } catch(ex) { console.log("Crypto error: " + ex) return null } return outputText }, // this constant string allows to distinguish whether some content // is encrypted with the algorithm used here // TODO: for updating the plugin in some TWs, detect old prefix (Cryptomx@) and warn prefix: "Sjcl@", encrypt: function(src, password) { if(!password) return src return this.prefix + this.callSjcl("encrypt", src, password) }, // if a wrong password is used, returns src as is decrypt: function(src, password) { var res = this.callSjcl("decrypt", src.substr(this.prefix.length), password) return res || src }, isEncrypted: function(src) { return src.substr(0, this.prefix.length) == this.prefix }, // these should not be found by indexOf() of this source; // as < is encoded as <, this won't happen anyway vaultAreaId: 'vaultArea', startSaveVaultArea: '
', endSaveVaultArea: '
', postVaultAreaMarker: '', // TODO: use this instead of createVaultArea, remove .. bits in updateOriginal // TODO: get rid of side-effects (alerts, displayMessage) addOrUpdateVaultArea: function(twHtml) { var posVault = this.locateVaultArea(twHtml) if(!posVault) { twHtml = this.createVaultArea(twHtml) alert(config.messages.vaultCreationInfo); posVault = this.locateVaultArea(twHtml) if(!posVault) alertAndThrow( config.messages.invalidFileError.format([localPath])); } var newVaultContent = !this.password ? "" : this.encrypt(store.allEncryptedTiddlersAsHtml(), this.password) var updatedHtml = twHtml.substr(0, posVault[0] + this.startSaveVaultArea.length) + convertUnicodeToUTF8(newVaultContent) + twHtml.substr(posVault[1]) displayMessage(config.messages[this.password ? 'vaultEncryptedInfo' : 'emptyVaultInfo']) return updatedHtml }, createVaultArea: function(original) { var revised = original.replace(//, '\n' + this.startSaveVaultArea + this.endSaveVaultArea + '\n' + this.postVaultAreaMarker) // TODO: insert into styleArea instead, use CSS comment as a marker // or even better add to PRE-HEAD var vaultStylesMarker = '' if(revised.indexOf(vaultStylesMarker) < 0) { var vaultStylesHtml = vaultStylesMarker + '\n\n\n' revised = revised.replace(//, vaultStylesHtml + '') } return revised }, // adapted from locateStoreArea locateVaultArea: function(original) { if(!original) return null // the vaultArea div should be just before the storeArea div var posOpeningDiv = original.indexOf(this.startSaveVaultArea) var limitClosingDiv = original.indexOf(this.postVaultAreaMarker) // startSaveArea is globally available (should be deprecated though) if(limitClosingDiv == -1) limitClosingDiv = original.indexOf(startSaveArea) var posClosingDiv = original.lastIndexOf(this.endSaveVaultArea, limitClosingDiv) return (posOpeningDiv == -1 || posClosingDiv == -1) ? null : [posOpeningDiv, posClosingDiv]; }, tagDontEncrypt: "unencrypted", tagForceEncrypt: "forceEncryption", getUnencryptedArray: function() { var unencryptedList = store.fetchTiddler("ListUnencrypted") if(!unencryptedList) return var filter = unencryptedList.text return store.filterTiddlers(filter) }, // this is used to avoid quadratic complexity (filtering N tiddlers per each N tiddlers) unencryptedCacheMap: null, populateUnencryptedCacheMap: function() { var unencryptedArray = this.getUnencryptedArray() this.unencryptedCacheMap = {} for(var i = 0; i < unencryptedArray.length; i++) { var title = unencryptedArray[i].title this.unencryptedCacheMap[title] = true } }, clearnUnencryptedCacheMap: function() { this.unencryptedCacheMap = null }, shouldEncryptTiddler: function(tiddler) { if(tiddler.isTagged(this.tagForceEncrypt)) return true if(store.isShadowTiddler([tiddler.title]) || tiddler.title === "ListUnencrypted" || tiddler.isTagged("systemConfig") || tiddler.isTagged(this.tagDontEncrypt) ) return false if(this.unencryptedCacheMap) { return !this.unencryptedCacheMap[tiddler.title] } else { var unencryptedArray = this.getUnencryptedArray() if(unencryptedArray && unencryptedArray.indexOf(tiddler) !== -1) return false /*var unencryptedList = store.fetchTiddler("ListUnencrypted") if(unencryptedList) { var filter = unencryptedList.text var tids = store.filterTiddlers(filter) if(tids.indexOf(tiddler) !== -1) return false }*/ } return true }, // TODO: review terms (locked, loaded, ..) – should be consistent getVaultContent: function() { var el = document.getElementById(this.vaultAreaId) return el ? el.innerHTML : null }, // TODO: remove state; may be move some sections above to ~submodules instead // loaded: falsy by default (we don't set it so that installing twice won't hurt) isLocked: function() { if(this.loaded) return false var vaultContent = this.getVaultContent() return vaultContent === null || this.isEncrypted(vaultContent) }, existsAndIsLocked: function() { return this.getVaultContent() !== null && this.isLocked() }, // TODO: fix lingo (missing, prompt); review // returns a boolean indicating success load: function() { if (!this.isLocked()) { // vaultAlreadyUnlockedWarning is missing even in the original plugin! alert(config.messages.vaultAlreadyUnlockedWarning); return false; } var vaultContent = this.getVaultContent() if(vaultContent === null) return false var pwd = this.password || ""; while(this.isEncrypted(vaultContent) && (pwd != null)) { if(pwd) vaultContent = this.decrypt(vaultContent, pwd); if(this.isEncrypted(vaultContent)) pwd = prompt("Enter a password", pwd); } if(pwd != null) this.password = pwd; if(this.isEncrypted(vaultContent)) return false; var wasDirty = store.isDirty(); if(vaultContent) { var e = document.createElement("div"); e.innerHTML = vaultContent; store.getLoader().loadTiddlers(store, e.childNodes); } this.loaded = true; refreshAll(); story.refreshAllTiddlers(); store.setDirty(wasDirty); return true }, // TODO: expose password setter, but hide direct access to password, if possible // password: falsy by default // TODO: can we implement a method "purge" instead? review current logic: // seems to have multiple flaws (one is: it's never restored to false!) // Make sure installing twice won't hurt shouldPurge: false, // save for decorating; avoid problems if this is installed twice originals: config.extensions.vault ? config.extensions.vault.originals : { updateOriginal: updateOriginal, LoaderBase_loadTiddler: LoaderBase.prototype.loadTiddler, saveChanges: saveChanges, Tiddler_doNotSave: Tiddler.prototype.doNotSave } } // TODO: try `this` instead of `store`; why `TiddlyWiki.prototype.allTiddlersAsHtml` uses that? TiddlyWiki.prototype.allUnencryptedTiddlersAsHtml = function() { Tiddler.prototype.doNotSave = function() { if(config.extensions.vault.shouldEncryptTiddler(this)) return true return config.extensions.vault.originals.Tiddler_doNotSave.apply(this, arguments) } var result = this.getSaver().externalize(store) Tiddler.prototype.doNotSave = config.extensions.vault.originals.Tiddler_doNotSave return result } TiddlyWiki.prototype.allEncryptedTiddlersAsHtml = function() { Tiddler.prototype.doNotSave = function() { if(!config.extensions.vault.shouldEncryptTiddler(this)) return true return config.extensions.vault.originals.Tiddler_doNotSave.apply(this, arguments) } var result = this.getSaver().externalize(store) Tiddler.prototype.doNotSave = config.extensions.vault.originals.Tiddler_doNotSave return result } // TODO: review all decorations; may be move (encapsulate) some bits to ceVault methods // decorate window.updateOriginal = function(original, posDiv) { var ceVault = config.extensions.vault var vaultIsUpdatable = !ceVault.locateVaultArea(original) || !ceVault.isLocked() || ceVault.shouldPurge ceVault.populateUnencryptedCacheMap() var orig_allTiddlersAsHtml = store.allTiddlersAsHtml //# or decorate prototype? store.allTiddlersAsHtml = function() { return convertUnicodeToUTF8((vaultIsUpdatable && ceVault.password) ? this.allUnencryptedTiddlersAsHtml() : orig_allTiddlersAsHtml.apply(this, arguments)) } var revised = ceVault.originals.updateOriginal.apply(this, arguments) if(vaultIsUpdatable) { // reports results via alert and/or displayMessage revised = ceVault.addOrUpdateVaultArea(revised) } else displayMessage(config.messages.vaultUnchangedInfo) store.allTiddlersAsHtml = orig_allTiddlersAsHtml ceVault.clearnUnencryptedCacheMap() return revised } // decorate LoaderBase.prototype.loadTiddler = function(store, node, tiddlers) { var title = this.getTitle(store, node); if(store.getTiddler(title) && !confirm(config.messages.confirmOverload +"\n\n"+ title)) return; return config.extensions.vault.originals.LoaderBase_loadTiddler.apply(this, arguments) } // decorate window.saveChanges = function(onlyIfDirty, tiddlers) { var ceVault = config.extensions.vault if(ceVault.shouldPurge || !ceVault.existsAndIsLocked() || confirm(config.messages.saveWithLockedVaultConfirm)) ceVault.originals.saveChanges.apply(this, arguments); } var shadowName = "StyleSheetVault" if(!config.shadowTiddlers[shadowName]) { config.shadowTiddlers[shadowName] = 'input { max-width: 100%; }' store.addNotification(shadowName, refreshStyles) store.addNotification("ColorPalette", function(smth, doc) { refreshStyles(shadowName, doc) }) } // TODO: move defaults to lingo config.macros.unlock = { handler: function(place, macroName, params, wikifier, paramString, tiddler) { var form = createTiddlyElement(place, 'form') jQuery(form).attr({ refresh: "macro", macroName: macroName }).data({ label: params[0] || "unlock vault", tooltip: params[1] || "unlock encrypted vault", openTiddlers: params[2] || "", closeTiddlers: params[3] || "", }) this.refresh(form) }, refresh: function(form) { var params = jQuery(form).data() jQuery(form).empty() var macro = this var ceVault = config.extensions.vault //# or may be show something more helpful if(!ceVault.existsAndIsLocked()) return; var input = createTiddlyElement(form, 'input', null, null, null, { type: 'password' }) input.focus() jQuery(input).on('keydown', function(event) { if(event.key !== 'Enter') return macro.unlockAndOpen(input.value, params.openTiddlers, params.closeTiddlers) return false }) // TODO: explain they can type and press "enter" createTiddlyButton(form, params.label, params.tooltip, function() { macro.unlockAndOpen(input.value, params.openTiddlers, params.closeTiddlers) return false }) }, unlockAndOpen: function(newPassword, openTiddlersFilter, closeTiddlersFilter) { var ceVault = config.extensions.vault if(newPassword) ceVault.password = newPassword if(ceVault.load()) { if(closeTiddlersFilter) { var tiddlers = store.filterTiddlers(closeTiddlersFilter) for(var i = 0; i < tiddlers.length; i++) { if(!story.isDirty(tiddlers[i].title)) story.closeTiddler(tiddlers[i].title) } } if(openTiddlersFilter) { var tiddlers = store.filterTiddlers(openTiddlersFilter) for(var i = 0; i < tiddlers.length; i++) story.displayTiddler("bottom", tiddlers[i].title) } } } } // TODO: move defaults to lingo config.macros.setPassword = { handler: function(place, macroName, params, wikifier, paramString, tiddler) { var form = createTiddlyElement(place, 'form') jQuery(form).attr({ refresh: "macro", macroName: macroName }).data({ label: params[0] || "set password", tooltip: params[1] || "Set password for encrypted vault" }) this.refresh(form) }, refresh: function(form) { var params = jQuery(form).data() jQuery(form).empty() if(config.extensions.vault.isLocked()) return createTiddlyButton(form, params.label, params.tooltip, this.onClick) }, onClick: function(event) { var form = event.target.parentElement jQuery(form).empty() var refreshForm = function() { config.macros.setPassword.refresh(form) } var input = createTiddlyElement(form, 'input', null, null, null, { type: 'password' }) input.focus() var ceVault = config.extensions.vault jQuery(input).on('keydown', function(event) { if(event.key === 'Escape') return refreshForm() if(event.key !== 'Enter') return ceVault.password = input.value refreshForm() displayMessage(ceVault.password ? config.messages.passwordSet : config.messages.passwordUnset) return false }) .on('blur', refreshForm) // TODO: explain they have to type and press "enter" and/or add a button for that // TODO: add also way to cancel (→ refresh form) return false; } } config.macros.purge = { handler: function(place, macroName, params, wikifier, paramString, tiddler) { var label = params[0] || "purge vault"; var tooltip = params[1] || "Delete locked vault"; var ceVault = config.extensions.vault if (ceVault.existsAndIsLocked()) createTiddlyButton(place, label, tooltip, this.onClick); }, onClick: function() { var ceVault = config.extensions.vault if (!ceVault.isLocked()) alert(config.messages.noLockedVaultNoPurge); else if(confirm(config.messages.purgeConfirm)) { ceVault.shouldPurge = true; alert(config.messages.vaultPurgedInfo); } return false; } } config.macros.ifLocked = { handler: function(place, macroName, params, wikifier, paramString, tiddler) { if(config.extensions.vault.existsAndIsLocked()) wikify(params[0], place, null, tiddler); } } config.macros.ifUnlocked = { handler: function(place, macroName, params, wikifier, paramString, tiddler) { if(config.extensions.vault.isLocked()) return; wikify(params[0], place, null, tiddler); } } //}}}