/***
|Name |EncryptionPlugin|
|Description |Allows to encrypt (password protect) selected content with AES and adapt the interface with additional macros|
|Demo |Open the [[demo page|https://yakovl.github.io/TiddlyWiki_EncryptionPlugin/demo.html]] to try the look & feel|
|Version |2.0.3|
|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]]); versions up to 1.7.1 were pre-released as ~NewEncryptedVaultPlugin|
|License|[[BSD open source license|License]]|
!Installation
While this plugin is supposed to work in any modern browser and with any TW saver, it is __recommended to create a backup__ of your TW before installing the plugin.
Other than that (and unlike v1.x), the plugin is __installed as usual__ (import/copy the plugin with the {{{systemConfig}}} tag, save and reload).
With [[ExtensionsExplorerPlugin|https://github.com/YakovL/TiddlyWiki_ExtensionsExplorerPlugin]], this one can be found and installed from the ~ExtensionsCollection → ~YLExtensionsCollection.
For details about upgrading from 1.x or migrating from ~EncryptedVaultPlugin, see sections below.
!Usage
* Use <<unlock>><<setPassword>> button (available by default in SideBarOptions)
** {{{<<unlock ButtonTitle ButtonTooltip OpenTiddlersWhenUnlock CloseTiddlersWhenUnlock>>}}} creates a button to unlock the encrypted vault (all parameters are optional)
** {{{<<setPassword ButtonTitle ButtonTooltip>>}}} if unlocked, creates a button to set the current password (all parameters are optional)
* Use a blank password to save unencrypted (disable vault usage)
* 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.
** Use {{{unencrypted}}} tag to avoid encryption for some tiddler
** Use {{{forceEncryption}}} tag to force some shadow tiddler to be encrypted
** Even shadow tiddlers (MainMenu, SiteTitle, PageTemplate, StyleSheet, ...) ''can be encrypted''. The shadow version is used until unlocking.
* Additionally, you can use the following macros:
** {{{<<purge ButtonTitle ButtonTooltip>>}}} if locked, creates a button to purge a locked vault, useful for lost password (encrypted content is the deleted)
** {{{<<ifLocked tiddlyText>>}}} displays tiddlyText (wikified) if the vault is locked
** {{{<<ifUnlocked tiddlyText>>}}} displays tiddlyText (wikified) if the vault is unlocked
!Understand your attack model
Security is a state, not a result, because it depends on actions of others. To understand if your security model is good enough, you should understnad the attack model.
A typical model is that you don't want somebody to have your data accidentally or with minimal efforts – for this case you have to just make sure the password you're using is strong enough (i.e. one can't quickly guess it or crack with simple dictionary techniques).
However, you should mind the data sensitivity: for instance, using encrypted TW to store credentials of financial accounts that hold big funds, or to store other information that "costs" much is not recommended as TW doesn't provide means to prevent brute force attacks, and can be theoretically attacked with other means, like malware plugins or phishing.
In other words, you should be mindful about what you are trying to protect, from whom, and if just encrypting with AES is good enough in your case.
!Upgrading from 1.x
Although it works smoothly, it is recommended to create a backup before upgrading from 1.x.
After upgrading, manual removal of the old vault is recommended: open TW with a text editor, find {{{<!--POST-SHADOWAREA-->}}}, and remove the 2 lines after it (one starting with {{{<div id="vaultArea">}}} and the other being {{{<!--POST-VAULTAREA-->}}}).
!Migration from ~EncryptedVaultPlugin
The recommended way to migrate is to
# Backup your TW
# Decrypt it (unset the password, save)
# Remove the ~EncryptedVaultPlugin, add ~EncryptionPlugin, save, reload
# Encrypt again (set the password, save, reload), check if it works fine
{{PoG{test thoroughly (including, DefaultTiddlers, first run of the plugin, macros appearence and usage, data format compatibility, including unencrypted, ...; .oO MVP for ...) and update metadata}}}
!Changes since ~EncryptedVaultPlugin
Compared to ~EncryptedVaultPlugin, this plugin:
* Uses a stonger encryption (AES);
* Fixes various major issues, uses decorators instead of overridings of several core functions;
* Uses a customizable StyleSheetVault shadow for styles;
* Improves the UI for password (doesn't show the typed value, adds autofocus, supports pressing enter to apply).
Additionally, versions 2.x use a tiddler to store encrypted content and hence:
* Are not restricted by which saver to use;
* Don't interfere with core upgrading (although in some cases one has to unencrypt TW before doing it);
* Encrypted content can be imported, [[inlcuded|https://yakovl.github.io/TiddlyWiki_SharedTiddlersPlugin/]], [[saved to a file|https://github.com/YakovL/TiddlyWiki_TiddlerInFilePlugin]], etc.
***/
/***
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<n;m++)h=a[e>>>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<<c)-1},concat:function(a,b){if(0===a.length||0===b.length)return a.concat(b);var c=a[a.length-1],d=sjcl.bitArray.getPartial(c);return 32===d?a.concat(b):sjcl.bitArray.$(b,d,c|0,a.slice(0,a.length-1))},bitLength:function(a){var b=a.length;return 0===
b?0:32*(b-1)+sjcl.bitArray.getPartial(a[b-1])},clamp:function(a,b){if(32*a.length<b)return a;a=a.slice(0,Math.ceil(b/32));var c=a.length;b=b&31;0<c&&b&&(a[c-1]=sjcl.bitArray.partial(b,a[c-1]&2147483648>>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<a.length;d++)c|=a[d]^b[d];return 0===
c},$:function(a,b,c,d){var e;e=0;for(void 0===d&&(d=[]);32<=b;b-=32)d.push(c),c=0;if(0===b)return d.concat(a);for(e=0;e<a.length;e++)d.push(c|a[e]>>>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<b+a?c:d.pop(),1));return d},i:function(a,b){return[a[0]^b[0],a[1]^b[1],a[2]^b[2],a[3]^b[3]]},byteswapM:function(a){var b,c;for(b=0;b<a.length;++b)c=a[b],a[b]=c>>>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<c/8;d++)0===(d&3)&&(e=a[d/4]),b+=String.fromCharCode(e>>>24),e<<=8;return decodeURIComponent(escape(b))},toBits:function(a){a=unescape(encodeURIComponent(a));var b=[],c,d=0;for(c=0;c<a.length;c++)d=d<<8|a.charCodeAt(c),3===(c&3)&&(b.push(d),d=0);c&3&&b.push(sjcl.bitArray.partial(8*(c&3),d));return b}};
sjcl.codec.hex={fromBits:function(a){var b="",c;for(c=0;c<a.length;c++)b+=((a[c]|0)+0xf00000000000).toString(16).substr(4);return b.substr(0,sjcl.bitArray.bitLength(a)/4)},toBits:function(a){var b,c=[],d;a=a.replace(/\s|0x/g,"");d=a.length;a=a+"00000000";for(b=0;b<a.length;b+=8)c.push(parseInt(a.substr(b,8),16)^0);return sjcl.bitArray.clamp(c,4*d)}};
sjcl.codec.base32={B:"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",X:"0123456789ABCDEFGHIJKLMNOPQRSTUV",BITS:32,BASE:5,REMAINING:27,fromBits:function(a,b,c){var d=sjcl.codec.base32.BASE,e=sjcl.codec.base32.REMAINING,f="",g=0,h=sjcl.codec.base32.B,k=0,l=sjcl.bitArray.bitLength(a);c&&(h=sjcl.codec.base32.X);for(c=0;f.length*d<l;)f+=h.charAt((k^a[c]>>>g)>>>e),g<d?(k=a[c]<<d-g,g+=e,c++):(k<<=d,g-=d);for(;f.length&7&&!b;)f+="=";return f},toBits:function(a,b){a=a.replace(/\s|=/g,"").toUpperCase();var c=sjcl.codec.base32.BITS,
d=sjcl.codec.base32.BASE,e=sjcl.codec.base32.REMAINING,f=[],g,h=0,k=sjcl.codec.base32.B,l=0,n,m="base32";b&&(k=sjcl.codec.base32.X,m="base32hex");for(g=0;g<a.length;g++){n=k.indexOf(a.charAt(g));if(0>n){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<<c-h):(h+=d,l^=n<<c-h)}h&56&&f.push(sjcl.bitArray.partial(h&56,l,1));return f}};
sjcl.codec.base32hex={fromBits:function(a,b){return sjcl.codec.base32.fromBits(a,b,1)},toBits:function(a){return sjcl.codec.base32.toBits(a,1)}};
sjcl.codec.base64={B:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",fromBits:function(a,b,c){var d="",e=0,f=sjcl.codec.base64.B,g=0,h=sjcl.bitArray.bitLength(a);c&&(f=f.substr(0,62)+"-_");for(c=0;6*d.length<h;)d+=f.charAt((g^a[c]>>>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;d<a.length;d++){h=f.indexOf(a.charAt(d));
if(0>h)throw new sjcl.exception.invalid("this isn't base64!");26<e?(e-=26,c.push(g^h>>>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(0x1fffffffffffff<a)throw new sjcl.exception.invalid("Cannot hash more than 2^53 - 1 bits");if("undefined"!==typeof Uint32Array){var d=new Uint32Array(c),e=0;for(b=512+b-(512+b&0x1ff);b<=a;b+=512)u(this,d.subarray(16*e,
16*(e+1))),e+=1;c.splice(0,16*e)}else for(b=512+b-(512+b&0x1ff);b<=a;b+=512)u(this,c.splice(0,16));return this},finalize:function(){var a,b=this.A,c=this.F,b=sjcl.bitArray.concat(b,[sjcl.bitArray.partial(1,1)]);for(a=b.length+2;a&15;a++)b.push(0);b.push(Math.floor(this.l/0x100000000));for(b.push(this.l|0);b.length;)u(this,b.splice(0,16));this.reset();return c},Y:[],b:[],O:function(){function a(a){return 0x100000000*(a-Math.floor(a))|0}for(var b=0,c=2,d,e;64>b;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);-1<a&&sjcl.mode.ccm.G.splice(a,1)},fa:function(a){var b=sjcl.mode.ccm.G.slice(),c;for(c=0;c<b.length;c+=1)b[c](a)},encrypt:function(a,b,c,d,e){var f,g=b.slice(0),h=sjcl.bitArray,k=h.bitLength(c)/8,l=h.bitLength(g)/8;e=e||64;d=d||[];if(7>k)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;b<g.length;b+=4)d=a.encrypt(k(d,g.slice(b,b+4).concat([0,0,0])));return d},V:function(a,b,c,d,e,f){var g=sjcl.bitArray,h=g.i;e/=8;if(e%2||4>e||16<e)throw new sjcl.exception.invalid("ccm: invalid tag length");
if(0xffffffff<d.length||0xffffffff<b.length)throw new sjcl.exception.bug("ccm: can't deal with 4GiB or more data");c=sjcl.mode.ccm.na(a,d,c,e,g.bitLength(b)/8,f);for(d=0;d<b.length;d+=4)c=a.encrypt(h(c,b.slice(d,d+4).concat([0,0,0])));return g.clamp(c,8*e)},C:function(a,b,c,d,e,f){var g,h=sjcl.bitArray;g=h.i;var k=b.length,l=h.bitLength(b),n=k/50,m=n;c=h.concat([h.partial(8,f-1)],c).concat([0,0,0]).slice(0,4);d=h.bitSlice(g(d,a.encrypt(c)),0,e);if(!k)return{tag:d,data:[]};for(g=0;g<k;g+=4)g>n&&(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+4<b.length;g+=4)m=b.slice(g,g+4),n=l(n,m),p=p.concat(l(c,a.encrypt(l(c,m)))),c=h(c);m=b.slice(g);b=k.bitLength(m);g=a.encrypt(l(c,[0,0,0,b]));m=k.clamp(l(m.concat([0,0,0]),g),b);n=l(n,l(m.concat([0,0,0]),g));n=a.encrypt(l(n,l(c,h(c))));
d.length&&(n=l(n,f?d:sjcl.mode.ocb2.pmac(a,d)));return p.concat(k.concat(m,k.clamp(n,e)))},decrypt:function(a,b,c,d,e,f){if(128!==sjcl.bitArray.bitLength(c))throw new sjcl.exception.invalid("ocb iv must be 128 bits");e=e||64;var g=sjcl.mode.ocb2.S,h=sjcl.bitArray,k=h.i,l=[0,0,0,0],n=g(a.encrypt(c)),m,p,r=sjcl.bitArray.bitLength(b)-e,q=[];d=d||[];for(c=0;c+4<r/32;c+=4)m=k(n,a.decrypt(k(n,b.slice(c,c+4)))),l=k(l,m),q=q.concat(m),n=g(n);p=r-32*c;m=a.encrypt(k(n,[0,0,0,p]));m=k(m,h.clamp(b.slice(c),p).concat([0,
0,0]));l=k(l,m);l=a.encrypt(k(l,k(n,g(n))));d.length&&(l=k(l,f?d:sjcl.mode.ocb2.pmac(a,d)));if(!h.equal(h.clamp(l,e),h.bitSlice(b,r)))throw new sjcl.exception.corrupt("ocb: tag doesn't match");return q.concat(h.clamp(m,p))},pmac:function(a,b){var c,d=sjcl.mode.ocb2.S,e=sjcl.bitArray,f=e.i,g=[0,0,0,0],h=a.encrypt([0,0,0,0]),h=f(h,d(d(h)));for(c=0;c+4<b.length;c+=4)h=d(h),g=f(g,a.encrypt(f(h,b.slice(c,c+4))));c=b.slice(c);128>e.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<d;d--)f[d]=f[d]>>>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;d<e;d+=4)b[0]^=0xffffffff&c[d],b[1]^=0xffffffff&c[d+1],b[2]^=0xffffffff&c[d+2],b[3]^=0xffffffff&c[d+3],b=sjcl.mode.gcm.ka(b,a);return b},C:function(a,b,c,d,e,f){var g,h,k,l,n,m,p,r,q=sjcl.bitArray;m=c.length;p=q.bitLength(c);r=q.bitLength(d);h=q.bitLength(e);
g=b.encrypt([0,0,0,0]);96===h?(e=e.slice(0),e=q.concat(e,[1])):(e=sjcl.mode.gcm.j(g,[0,0,0,0],e),e=sjcl.mode.gcm.j(g,e,[0,0,Math.floor(h/0x100000000),h&0xffffffff]));h=sjcl.mode.gcm.j(g,[0,0,0,0],d);n=e.slice(0);d=h.slice(0);a||(d=sjcl.mode.gcm.j(g,h,c));for(l=0;l<m;l+=4)n[3]++,k=b.encrypt(n),c[l]^=k[0],c[l+1]^=k[1],c[l+2]^=k[2],c[l+3]^=k[3];c=q.clamp(c,p);a&&(d=sjcl.mode.gcm.j(g,h,c));a=[Math.floor(r/0x100000000),r&0xffffffff,Math.floor(p/0x100000000),p&0xffffffff];d=sjcl.mode.gcm.j(g,d,a);k=b.encrypt(e);
d[0]^=k[0];d[1]^=k[1];d[2]^=k[2];d[3]^=k[3];return{tag:q.bitSlice(d,0,f),data:c}}};sjcl.misc.hmac=function(a,b){this.W=b=b||sjcl.hash.sha256;var c=[[],[]],d,e=b.prototype.blockSize/32;this.w=[new b,new b];a.length>e&&(a=b.hash(a));for(d=0;d<e;d++)c[0][d]=a[d]^909522486,c[1][d]=a[d]^1549556828;this.w[0].update(c[0]);this.w[1].update(c[1]);this.R=new b(this.w[0])};
sjcl.misc.hmac.prototype.encrypt=sjcl.misc.hmac.prototype.mac=function(a){if(this.aa)throw new sjcl.exception.invalid("encrypt on already updated hmac called!");this.update(a);return this.digest(a)};sjcl.misc.hmac.prototype.reset=function(){this.R=new this.W(this.w[0]);this.aa=!1};sjcl.misc.hmac.prototype.update=function(a){this.aa=!0;this.R.update(a)};sjcl.misc.hmac.prototype.digest=function(){var a=this.R.finalize(),a=(new this.W(this.w[1])).update(a).finalize();this.reset();return a};
sjcl.misc.pbkdf2=function(a,b,c,d,e){c=c||1E4;if(0>d||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;g<c;g++)for(f=a.encrypt(f),h=0;h<f.length;h++)e[h]^=f[h];l=l.concat(e)}d&&(l=n.clamp(l,d));return l};
sjcl.prng=function(a){this.c=[new sjcl.hash.sha256];this.m=[0];this.P=0;this.H={};this.N=0;this.U={};this.Z=this.f=this.o=this.ha=0;this.b=[0,0,0,0,0,0,0,0];this.h=[0,0,0,0];this.L=void 0;this.M=a;this.D=!1;this.K={progress:{},seeded:{}};this.u=this.ga=0;this.I=1;this.J=2;this.ca=0x10000;this.T=[0,48,64,96,128,192,0x100,384,512,768,1024];this.da=3E4;this.ba=80};
sjcl.prng.prototype={randomWords:function(a,b){var c=[],d;d=this.isReady(b);var e;if(d===this.u)throw new sjcl.exception.notReady("generator isn't seeded");if(d&this.J){d=!(d&this.I);e=[];var f=0,g;this.Z=e[0]=(new Date).valueOf()+this.da;for(g=0;16>g;g++)e.push(0x100000000*Math.random()|0);for(g=0;g<this.c.length&&(e=e.concat(this.c[g].finalize()),f+=this.m[g],this.m[g]=0,d||!(this.P&1<<g));g++);this.P>=1<<this.c.length&&(this.c.push(new sjcl.hash.sha256),this.m.push(0));this.f-=f;f>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<a;d+=4)0===(d+1)%this.ca&&y(this),e=z(this),c.push(e[0],e[1],e[2],e[3]);y(this);return c.slice(0,a)},setDefaultParanoia:function(a,b){if(0===a&&"Setting paranoia=0 will ruin your security; use it only for testing"!==b)throw new sjcl.exception.invalid("Setting paranoia=0 will ruin your security; use it only for testing");this.M=a},addEntropy:function(a,
b,c){c=c||"user";var d,e,f=(new Date).valueOf(),g=this.H[c],h=this.isReady(),k=0;d=this.U[c];void 0===d&&(d=this.U[c]=this.ha++);void 0===g&&(g=this.H[c]=0);this.H[c]=(this.H[c]+1)%this.c.length;switch(typeof a){case "number":void 0===b&&(b=1);this.c[g].update([d,this.N++,1,b,f,1,a|0]);break;case "object":c=Object.prototype.toString.call(a);if("[object Uint32Array]"===c){e=[];for(c=0;c<a.length;c++)e.push(a[c]);a=e}else for("[object Array]"!==c&&(k=1),c=0;c<a.length&&!k;c++)"number"!==typeof a[c]&&
(k=1);if(!k){if(void 0===b)for(c=b=0;c<a.length;c++)for(e=a[c];0<e;)b++,e=e>>>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;c<f.length;c++)d=f[c],delete e[d]},la:function(){C(this,1)},oa:function(a){var b,c;try{b=a.x||a.clientX||a.offsetX||0,c=a.y||a.clientY||a.offsetY||0}catch(d){c=b=0}0!=b&&0!=c&&this.addEntropy([b,c],2,"mouse");C(this,0)},qa:function(a){a=
a.touches[0]||a.changedTouches[0];this.addEntropy([a.pageX||a.clientX,a.pageY||a.clientY],1,"touch");C(this,0)},ma:function(){C(this,2)},ea:function(a){a=a.accelerationIncludingGravity.x||a.accelerationIncludingGravity.y||a.accelerationIncludingGravity.z;if(window.orientation){var b=window.orientation;"number"===typeof b&&this.addEntropy(b,1,"accelerometer")}a&&this.addEntropy(a,2,"accelerometer");C(this,0)}};
function A(a,b){var c,d=sjcl.random.K[a],e=[];for(c in d)d.hasOwnProperty(c)&&e.push(d[c]);for(c=0;c<e.length;c++)e[c](b)}function C(a,b){"undefined"!==typeof window&&window.performance&&"function"===typeof window.performance.now?a.addEntropy(window.performance.now(),b,"loadtime"):a.addEntropy((new Date).valueOf(),b,"loadtime")}function y(a){a.b=z(a).concat(z(a));a.L=new sjcl.cipher.aes(a.b)}function z(a){for(var b=0;4>b&&(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<f.iv.length)throw new sjcl.exception.invalid("json encrypt: invalid parameters");"string"===typeof a?(g=sjcl.misc.cachedPbkdf2(a,f),a=g.key.slice(0,f.ks/32),f.salt=g.salt):sjcl.ecc&&a instanceof sjcl.ecc.elGamal.publicKey&&(g=a.kem(),f.kemtag=g.tag,a=g.key.slice(0,f.ks/32));"string"===typeof b&&(b=sjcl.codec.utf8String.toBits(b));"string"===typeof c&&(f.adata=c=sjcl.codec.utf8String.toBits(c));g=new sjcl.cipher[f.cipher](a);e.g(d,f);d.key=a;f.ct="ccm"===f.mode&&sjcl.arrayBuffer&&sjcl.arrayBuffer.ccm&&
b instanceof ArrayBuffer?sjcl.arrayBuffer.ccm.encrypt(g,b,f.iv,c,f.ts):sjcl.mode[f.mode].encrypt(g,b,f.iv,c,f.ts);return f},encrypt:function(a,b,c,d){var e=sjcl.json,f=e.ja.apply(e,arguments);return e.encode(f)},ia:function(a,b,c,d){c=c||{};d=d||{};var e=sjcl.json;b=e.g(e.g(e.g({},e.defaults),b),c,!0);var f,g;f=b.adata;"string"===typeof b.salt&&(b.salt=sjcl.codec.base64.toBits(b.salt));"string"===typeof b.iv&&(b.iv=sjcl.codec.base64.toBits(b.iv));if(!sjcl.mode[b.mode]||!sjcl.cipher[b.cipher]||"string"===
typeof a&&100>=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<b.iv.length)throw new sjcl.exception.invalid("json decrypt: invalid parameters");"string"===typeof a?(g=sjcl.misc.cachedPbkdf2(a,b),a=g.key.slice(0,b.ks/32),b.salt=g.salt):sjcl.ecc&&a instanceof sjcl.ecc.elGamal.secretKey&&(a=a.unkem(sjcl.codec.base64.toBits(b.kemtag)).slice(0,b.ks/32));"string"===typeof f&&(f=sjcl.codec.utf8String.toBits(f));g=new sjcl.cipher[b.cipher](a);f="ccm"===
b.mode&&sjcl.arrayBuffer&&sjcl.arrayBuffer.ccm&&b.ct instanceof ArrayBuffer?sjcl.arrayBuffer.ccm.decrypt(g,b.ct,b.iv,b.tag,f,b.ts):sjcl.mode[b.mode].decrypt(g,b.ct,b.iv,f,b.ts);e.g(d,b);d.key=a;return 1===c.raw?f:sjcl.codec.utf8String.fromBits(f)},decrypt:function(a,b,c,d){var e=sjcl.json;return e.ia(a,e.decode(b),c,d)},encode:function(a){var b,c="{",d="";for(b in a)if(a.hasOwnProperty(b)){if(!b.match(/^[a-z0-9]+$/i))throw new sjcl.exception.invalid("json encode: invalid property name");c+=d+'"'+
b+'":';d=",";switch(typeof a[b]){case "number":case "boolean":c+=a[b];break;case "string":c+='"'+escape(a[b])+'"';break;case "object":c+='"'+sjcl.codec.base64.fromBits(a[b],0)+'"';break;default:throw new sjcl.exception.bug("json encode: unsupported type");}}return c+"}"},decode:function(a){a=a.replace(/\s/g,"");if(!a.match(/^\{.*\}$/))throw new sjcl.exception.invalid("json decode: this isn't json!");a=a.replace(/^\{|\}$/g,"").split(/,/);var b={},c,d;for(c=0;c<a.length;c++){if(!(d=a[c].match(/^\s*(?:(["']?)([a-z][a-z0-9]*)\1)\s*:\s*(?:(-?\d+)|"([a-z0-9+\/%*_.@=\-]*)"|(true|false))$/i)))throw new sjcl.exception.invalid("json decode: this isn't json!");
null!=d[3]?b[d[2]]=parseInt(d[3],10):null!=d[4]?b[d[2]]=d[2].match(/^(ct|adata|salt|iv)$/)?sjcl.codec.base64.toBits(d[4]):unescape(d[4]):null!=d[5]&&(b[d[2]]="true"===d[5])}return b},g:function(a,b,c){void 0===a&&(a={});if(void 0===b)return a;for(var d in b)if(b.hasOwnProperty(d)){if(c&&void 0!==a[d]&&a[d]!==b[d])throw new sjcl.exception.invalid("required parameter overridden");a[d]=b[d]}return a},sa:function(a,b){var c={},d;for(d in a)a.hasOwnProperty(d)&&a[d]!==b[d]&&(c[d]=a[d]);return c},ra:function(a,
b){var c={},d;for(d=0;d<b.length;d++)void 0!==a[b[d]]&&(c[b[d]]=a[b[d]]);return c}};sjcl.encrypt=sjcl.json.encrypt;sjcl.decrypt=sjcl.json.decrypt;sjcl.misc.pa={};sjcl.misc.cachedPbkdf2=function(a,b){var c=sjcl.misc.pa,d;b=b||{};d=b.iter||1E3;c=c[a]=c[a]||{};d=c[d]=c[d]||{firstSalt:b.salt&&b.salt.length?b.salt.slice(0):sjcl.random.randomWords(2,0)};c=void 0===b.salt?d.firstSalt:b.salt;d[c]=d[c]||sjcl.misc.pbkdf2(a,c,b.iter);return{key:d[c].slice(0),salt:c.slice(0)}};
"undefined"!==typeof module&&module.exports&&(module.exports=sjcl);"function"===typeof define&&define([],function(){return sjcl});
window.sjcl = sjcl;
//}}}
//{{{
// make plugin work with MainTiddlyServer and TW 2.8.1+
config.options.chkAvoidGranulatedSaving = true;
config.shadowTiddlers.SideBarOptions = config.shadowTiddlers.SideBarOptions.replace(
/<<saveChanges>>/, "<<unlock>><<setPassword>><<saveChanges>>");
config.shadowTiddlers.GettingStarted +=
"\n\n<<ifLocked 'This ~TiddlyWiki uses EncryptionPlugin. " +
"To load protected content click on'>><<unlock>>" +
"<<ifUnlocked 'This ~TiddlyWiki uses EncryptionPlugin. " +
"To set or change password click on'>><<setPassword>>";
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 === null ? src : res
},
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: '<div id="vaultArea">',
endSaveVaultArea: '</div>',
postVaultAreaMarker: '<!--POST-VAULTAREA-->',
// 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];
},
// implementing in-tiddler vault
singleVaultTiddlerName: "EncryptedVault",
updateVaultTiddler: function(encryptedContentOrNull) {
if(encryptedContentOrNull === null) {
// TODO: ask for confirmation? make undo-able?
store.deleteTiddler(this.singleVaultTiddlerName)
return
}
var encryptedContent = encryptedContentOrNull
var vaultTiddler = store.fetchTiddler(this.singleVaultTiddlerName) // fetch existing or create
if(!vaultTiddler) {
vaultTiddler = new Tiddler(this.singleVaultTiddlerName)
vaultTiddler.tags.push(this.tagDontEncrypt)
store.addTiddler(vaultTiddler) // doesn't setDirty, unlike createTiddler
}
vaultTiddler.text = encryptedContent
vaultTiddler.modified = new Date()
// TODO: update modifier? (see core methods that update the fields) something else?
},
getVaultTiddlerContent: function() {
var vaultTiddler = store.fetchTiddler(this.singleVaultTiddlerName)
return vaultTiddler ? vaultTiddler.text : null
// same, but returning null is implicit: return store.getTiddlerText(this.singleVaultTiddlerName)
},
tagDontEncrypt: "unencrypted",
tagForceEncrypt: "forceEncryption",
getUnencryptedArray: function() {
var unencryptedList = store.fetchTiddler("ListUnencrypted")
return !unencryptedList ? [] :
store.filterTiddlers(unencryptedList.text)
},
// 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.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 inTiddlerContent = this.getVaultTiddlerContent()
if(inTiddlerContent) return inTiddlerContent
// backward compatibility with 1.x
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.existsAndIsLocked()) {
// 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
},
doNotSaveMode: 'encrypted'
}
// decorate
Tiddler.prototype.doNotSave = function() {
var shouldEncrypt = config.extensions.vault.shouldEncryptTiddler(this)
var mode = config.extensions.vault.doNotSaveMode
if((shouldEncrypt && mode == "encrypted" || !shouldEncrypt && mode == "unencrypted") &&
config.extensions.vault.password) return true
return config.extensions.vault.originals.Tiddler_doNotSave.apply(this, arguments)
}
TiddlyWiki.prototype.allEncryptedTiddlersAsHtml = function() {
var prevMode = config.extensions.vault.doNotSaveMode
config.extensions.vault.doNotSaveMode = "unencrypted"
var result = this.allTiddlersAsHtml()
config.extensions.vault.doNotSaveMode = prevMode
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.existsAndIsLocked(true) || ceVault.shouldPurge
ceVault.populateUnencryptedCacheMap()
var password = ceVault.password
ceVault.updateVaultTiddler(!password ? null : ceVault.encrypt(store.allEncryptedTiddlersAsHtml(), password))
var revised = ceVault.originals.updateOriginal.apply(this, arguments)
//# move reporting results outside updateOriginal?
if(vaultIsUpdatable) {
displayMessage(config.messages[password ? 'vaultEncryptedInfo' : 'emptyVaultInfo'])
} else
displayMessage(config.messages.vaultUnchangedInfo)
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.existsAndIsLocked()) 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.existsAndIsLocked())
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.existsAndIsLocked())
wikify(params[0], place, null, tiddler);
}
}
//}}}