diff --git a/plugins/tiddlypwa/config.tid b/plugins/tiddlypwa/config.tid
index 7284fa6..0839f59 100644
--- a/plugins/tiddlypwa/config.tid
+++ b/plugins/tiddlypwa/config.tid
@@ -42,6 +42,9 @@ The password (or technically, the encryption key derived from it) is currently
''not remembered'' on this device. <$button message="tiddlypwa-remember">Remember it$button>?
$reveal>
+The salt value for this wiki is: {{$:/status/TiddlyPWASalt}}
+(if you were to manually initialize a new one to sync with this one, you would need to copy this value).
+
!! Synchronization
{{$:/plugins/valpackett/tiddlypwa/sync-status}}
diff --git a/plugins/tiddlypwa/main.js b/plugins/tiddlypwa/main.js
index bea9e0e..01f0b83 100644
--- a/plugins/tiddlypwa/main.js
+++ b/plugins/tiddlypwa/main.js
@@ -50,7 +50,7 @@ Formatted with `deno fmt`.
function b64dec(base64) {
// welp touching binary strings here but seems to be a decent compact way
- return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)).buffer;
+ return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
}
function formatBytes(bytes) {
@@ -355,6 +355,7 @@ Formatted with `deno fmt`.
}
initDb(db) {
+ db.createObjectStore('metadata', { autoIncrement: true });
db.createObjectStore('session', { autoIncrement: true });
db.createObjectStore('syncservers', { autoIncrement: true });
db.createObjectStore('tiddlers', { keyPath: 'thash' });
@@ -389,6 +390,12 @@ Formatted with `deno fmt`.
this.db.transaction('tiddlers').objectStore('tiddlers').openCursor().onsuccess = (evt) =>
resolve(!evt.target.result)
);
+ if (!this.salt) {
+ const meta = await adb(this.db.transaction('metadata').objectStore('metadata').getAll());
+ if (meta.length > 0) {
+ this.salt = meta[meta.length - 1].salt;
+ }
+ }
if (!this.key) {
const ses = await adb(this.db.transaction('session').objectStore('session').getAll());
if (ses.length > 0) {
@@ -457,13 +464,16 @@ Formatted with `deno fmt`.
signal: giveUp.signal,
cache: 'no-store',
});
- const { state, endpoint } = await resp.json();
- if ((endpoint && typeof endpoint !== 'string') || (state && typeof state !== 'string')) {
+ const { state, endpoint, salt } = await resp.json();
+ if (
+ (endpoint && typeof endpoint !== 'string') || (state && typeof state !== 'string') ||
+ (salt && typeof salt !== 'string')
+ ) {
alert('Something is weird with the server! Unexpected types in bootstrap.json');
}
bootstrapEndpoint = endpoint && { url: endpoint };
clearTimeout(timeoutGiveUpBtn);
- let askToken = true;
+ let askToken = true, askSalt = true;
if (state === 'docs') {
this.db.close();
closeModal();
@@ -490,6 +500,8 @@ Formatted with `deno fmt`.
body.innerHTML = '
Welcome back to your synchronized wiki!
'; body.innerHTML += `Log in using your credentials below. You are using the sync server ${endpoint}.
We are not quite sure what happened on the sync server...
'; body.innerHTML += `Try to log in using your credentials below anyway?
`; @@ -498,7 +510,7 @@ Formatted with `deno fmt`. if (!bootstrapEndpoint) { alert(`This sync server is misconfigured: no endpoint found while state is '${state}'.`); } - const tokLbl = dm('label', { innerHTML: 'Sync token' }); + const tokLbl = dm('label', { text: 'Sync token' }); tokLbl.appendChild(dm('input', { attributes: { type: 'password' }, eventListeners: [{ @@ -508,6 +520,32 @@ Formatted with `deno fmt`. })); form.appendChild(tokLbl); } + if (askSalt) { + const saltDtl = dm('details', { + innerHTML: ` +In order for such a sync to succeed, the wiki needs to be initialized with the same "salt" as well as the same password.
+Copy the salt from the Settings → Storage and Sync page on the existing wiki, or from the sync admin interface.
+ `, + }); + const saltLbl = dm('label', { text: 'Salt' }); + saltLbl.appendChild(dm('input', { + attributes: { type: 'text' }, + eventListeners: [{ + name: 'change', + handlerFunction: (e) => { + try { + this.salt = b64dec(e.target.value.trim()); + feedback.innerHTML = ''; + } catch (_e) { + feedback.innerHTML = 'Could not decode the salt
'; + } + }, + }], + })); + saltDtl.appendChild(saltLbl); + form.appendChild(saltDtl); + } showForm(); openModal(); } catch (e) { @@ -535,7 +573,8 @@ Formatted with `deno fmt`. resolve(); }; }); - const basebits = await argon.hash(utfenc.encode(passInput.value), utfenc.encode('tiddly.pwa.storage')); + if (!this.salt) this.salt = crypto.getRandomValues(new Uint8Array(32)); + const basebits = await argon.hash(utfenc.encode(passInput.value), this.salt); const basekey = await crypto.subtle.importKey('raw', basebits, 'HKDF', false, ['deriveKey']); // fun: https://soatok.blog/2021/11/17/understanding-hkdf/ (but we don't have any randomness to shove into info) this.key = await crypto.subtle.deriveKey( @@ -560,7 +599,9 @@ Formatted with `deno fmt`. } } argon.terminate(); - closeModal(); + if (freshDb) { + this.db.transaction('metadata', 'readwrite').objectStore('metadata').put({ salt: this.salt }); + } if (bootstrapEndpoint) { const { url, token } = bootstrapEndpoint; await adb( @@ -571,11 +612,16 @@ Formatted with `deno fmt`. }), ); } + closeModal(); } else { await this.initialRead(); } await this.reflectSyncServers(); await this.reflectStorageInfo(); + this.wiki.addTiddler({ + title: '$:/status/TiddlyPWASalt', + text: await b64enc(this.salt), + }); } getStatus(cb) { @@ -855,6 +901,7 @@ Formatted with `deno fmt`. token, browserToken: this.browserToken, authcode: await b64enc(await this.titlehash(token)), + salt: await b64enc(this.salt), now: new Date(), // only for a desync check lastSync, clientChanges, diff --git a/plugins/tiddlypwa/style.css b/plugins/tiddlypwa/style.css index 1f28f7b..be40734 100644 --- a/plugins/tiddlypwa/style.css +++ b/plugins/tiddlypwa/style.css @@ -7,5 +7,6 @@ Licensed under 0BSD, see license.tid. \*/ .tc-modal-wrapper { backdrop-filter: blur(3px); } -.tiddlypwa-form input, .tiddlypwa-form button { display: block; width: 100%; margin: 0.5em 0; padding: 0.25em; } +.tiddlypwa-form input, .tiddlypwa-form button, .tiddlypwa-form details { display: block; width: 100%; margin: 0.5em 0; } +.tiddlypwa-form input, .tiddlypwa-form button { padding: 0.25em; border-radius: 3px; border: 1px solid #999; } .tiddlypwa-form-error { color: #ee1111; font-weight: bolder; }