diff --git a/docs/setup_own_server.md b/docs/setup_own_server.md index c93164e..6c5bbba 100644 --- a/docs/setup_own_server.md +++ b/docs/setup_own_server.md @@ -108,13 +108,18 @@ $ export database=obsidiannotes #Please change as you like $ export passphrase=dfsapkdjaskdjasdas #Please change as you like $ deno run -A https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/flyio/generate_setupuri.ts obsidian://setuplivesync?settings=%5B%22tm2DpsOE74nJAryprZO2M93wF%2Fvg.......4b26ed33230729%22%5D + +Your passphrase of Setup-URI is: patient-haze +This passphrase is never shown again, so please note it in a safe place. ``` +Please keep your passphrase of Setup-URI. + ### 2. Setup Self-hosted LiveSync to Obsidian [This video](https://youtu.be/7sa_I1832Xc?t=146) may help us. 1. Install Self-hosted LiveSync 2. Choose `Use the copied setup URI` from the command palette and paste the setup URI. (obsidian://setuplivesync?settings=.....). -3. Type `welcome` for setup-uri passphrase. +3. Type the previously displayed passphrase (`patient-haze`) for setup-uri passphrase. 4. Answer `yes` and `Set it up...`, and finish the first dialogue with `Keep them disabled`. 5. `Reload app without save` once. diff --git a/setup-flyio-on-the-fly-v2.ipynb b/setup-flyio-on-the-fly-v2.ipynb index 787eca2..c2c6545 100644 --- a/setup-flyio-on-the-fly-v2.ipynb +++ b/setup-flyio-on-the-fly-v2.ipynb @@ -1,27 +1,10 @@ { - "nbformat": 4, - "nbformat_minor": 0, - "metadata": { - "colab": { - "provenance": [], - "private_outputs": true, - "authorship_tag": "ABX9TyMexQ5pErH5LBG2tENtEVWf", - "include_colab_link": true - }, - "kernelspec": { - "name": "python3", - "display_name": "Python 3" - }, - "language_info": { - "name": "python" - } - }, "cells": [ { "cell_type": "markdown", "metadata": { - "id": "view-in-github", - "colab_type": "text" + "colab_type": "text", + "id": "view-in-github" }, "source": [ "\"Open" @@ -29,12 +12,12 @@ }, { "cell_type": "markdown", - "source": [ - "- Initial version 7th Feb. 2024" - ], "metadata": { "id": "AzLlAcLFRO5A" - } + }, + "source": [ + "- Initial version 7th Feb. 2024" + ] }, { "cell_type": "code", @@ -55,27 +38,32 @@ }, { "cell_type": "code", - "source": [ - "# Login up sign up\n", - "!flyctl auth signup" - ], + "execution_count": null, "metadata": { "id": "mGN08BaFDviy" }, - "execution_count": null, - "outputs": [] + "outputs": [], + "source": [ + "# Login up sign up\n", + "!flyctl auth signup" + ] }, { "cell_type": "markdown", - "source": [ - "Select a region and execute the block." - ], "metadata": { "id": "BBFTFOP6vA8m" - } + }, + "source": [ + "Select a region and execute the block." + ] }, { "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TNl0A603EF9E" + }, + "outputs": [], "source": [ "# see https://fly.io/docs/reference/regions/\n", "region = \"nrt/Tokyo, Japan\" #@param [\"ams/Amsterdam, Netherlands\",\"arn/Stockholm, Sweden\",\"atl/Atlanta, Georgia (US)\",\"bog/Bogotá, Colombia\",\"bos/Boston, Massachusetts (US)\",\"cdg/Paris, France\",\"den/Denver, Colorado (US)\",\"dfw/Dallas, Texas (US)\",\"ewr/Secaucus, NJ (US)\",\"eze/Ezeiza, Argentina\",\"gdl/Guadalajara, Mexico\",\"gig/Rio de Janeiro, Brazil\",\"gru/Sao Paulo, Brazil\",\"hkg/Hong Kong, Hong Kong\",\"iad/Ashburn, Virginia (US)\",\"jnb/Johannesburg, South Africa\",\"lax/Los Angeles, California (US)\",\"lhr/London, United Kingdom\",\"mad/Madrid, Spain\",\"mia/Miami, Florida (US)\",\"nrt/Tokyo, Japan\",\"ord/Chicago, Illinois (US)\",\"otp/Bucharest, Romania\",\"phx/Phoenix, Arizona (US)\",\"qro/Querétaro, Mexico\",\"scl/Santiago, Chile\",\"sea/Seattle, Washington (US)\",\"sin/Singapore, Singapore\",\"sjc/San Jose, California (US)\",\"syd/Sydney, Australia\",\"waw/Warsaw, Poland\",\"yul/Montreal, Canada\",\"yyz/Toronto, Canada\" ] {allow-input: true}\n", @@ -98,31 +86,29 @@ " last_line = str.strip(last_line)\n", "\n", "if last_line.startswith(\"obsidian://\"):\n", - " result = HTML(f\"Copy your setup-URI with this button! ->
Importing passphrase is `welcome`.
If you want to synchronise in live mode, please apply a preset after ensuring the imported configuration works.\")\n", + " result = HTML(f\"Copy your setup-URI with this button! ->
Importing passphrase is displayed one.
If you want to synchronise in live mode, please apply a preset after ensuring the imported configuration works.\")\n", "else:\n", " result = \"Failed to encrypt the setup URI\"\n", "result" - ], - "metadata": { - "id": "TNl0A603EF9E" - }, - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "markdown", + "metadata": { + "id": "oeIzExnEKhFp" + }, "source": [ "If you see the `Copy setup URI` button, Congratulations! Your CouchDB is ready to use! Please click the button. And open this on Obsidian.\n", "\n", "And, you should keep the output to your secret memo.\n", "\n" - ], - "metadata": { - "id": "oeIzExnEKhFp" - } + ] }, { "cell_type": "markdown", + "metadata": { + "id": "sdQrqOjERN3K" + }, "source": [ "\n", "\n", @@ -131,21 +117,35 @@ "\n", "If you want to delete this CouchDB instance, you can do it by executing next cell. \n", "If your fly.toml has been gone, access https://fly.io/dashboard and check the existing app." - ], - "metadata": { - "id": "sdQrqOjERN3K" - } + ] }, { "cell_type": "code", - "source": [ - "!./delete-server.sh" - ], + "execution_count": null, "metadata": { "id": "7JMSkNvVIIfg" }, - "execution_count": null, - "outputs": [] + "outputs": [], + "source": [ + "!./delete-server.sh" + ] } - ] -} \ No newline at end of file + ], + "metadata": { + "colab": { + "authorship_tag": "ABX9TyMexQ5pErH5LBG2tENtEVWf", + "include_colab_link": true, + "private_outputs": true, + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/utils/flyio/deploy-server.sh b/utils/flyio/deploy-server.sh index 56476ec..967c176 100755 --- a/utils/flyio/deploy-server.sh +++ b/utils/flyio/deploy-server.sh @@ -26,7 +26,7 @@ echo "OK!" if command -v deno >/dev/null 2>&1; then echo "Setup finished! Also, we can set up Self-hosted LiveSync instantly, by the following setup uri." - echo "Passphrase of setup-uri is \`welcome\`". + echo "Passphrase of setup-uri will be printed only one time. Keep it safe!" echo "--- configured ---" echo "database : ${database}" echo "E2EE passphrase: ${passphrase}" diff --git a/utils/flyio/generate_setupuri.ts b/utils/flyio/generate_setupuri.ts index 03bb607..61ee86e 100644 --- a/utils/flyio/generate_setupuri.ts +++ b/utils/flyio/generate_setupuri.ts @@ -1,153 +1,13 @@ -import { webcrypto } from "node:crypto"; +import { encrypt } from "npm:octagonal-wheels@0.1.11/encryption/encryption.js"; -const KEY_RECYCLE_COUNT = 100; -type KeyBuffer = { - key: CryptoKey; - salt: Uint8Array; - count: number; -}; - -let semiStaticFieldBuffer: Uint8Array; -const nonceBuffer: Uint32Array = new Uint32Array(1); -const writeString = (string: string) => { - // Prepare enough buffer. - const buffer = new Uint8Array(string.length * 4); - const length = string.length; - let index = 0; - let chr = 0; - let idx = 0; - while (idx < length) { - chr = string.charCodeAt(idx++); - if (chr < 128) { - buffer[index++] = chr; - } else if (chr < 0x800) { - // 2 bytes - buffer[index++] = 0xC0 | (chr >>> 6); - buffer[index++] = 0x80 | (chr & 0x3F); - } else if (chr < 0xD800 || chr > 0xDFFF) { - // 3 bytes - buffer[index++] = 0xE0 | (chr >>> 12); - buffer[index++] = 0x80 | ((chr >>> 6) & 0x3F); - buffer[index++] = 0x80 | (chr & 0x3F); - } else { - // 4 bytes - surrogate pair - chr = (((chr - 0xD800) << 10) | (string.charCodeAt(idx++) - 0xDC00)) + 0x10000; - buffer[index++] = 0xF0 | (chr >>> 18); - buffer[index++] = 0x80 | ((chr >>> 12) & 0x3F); - buffer[index++] = 0x80 | ((chr >>> 6) & 0x3F); - buffer[index++] = 0x80 | (chr & 0x3F); - } - } - return buffer.slice(0, index); -}; -const KeyBuffs = new Map(); -async function getKeyForEncrypt(passphrase: string, autoCalculateIterations: boolean): Promise<[CryptoKey, Uint8Array]> { - // For performance, the plugin reuses the key KEY_RECYCLE_COUNT times. - const buffKey = `${passphrase}-${autoCalculateIterations}`; - const f = KeyBuffs.get(buffKey); - if (f) { - f.count--; - if (f.count > 0) { - return [f.key, f.salt]; - } - f.count--; - } - const passphraseLen = 15 - passphrase.length; - const iteration = autoCalculateIterations ? ((passphraseLen > 0 ? passphraseLen : 0) * 1000) + 121 - passphraseLen : 100000; - const passphraseBin = new TextEncoder().encode(passphrase); - const digest = await webcrypto.subtle.digest({ name: "SHA-256" }, passphraseBin); - const keyMaterial = await webcrypto.subtle.importKey("raw", digest, { name: "PBKDF2" }, false, ["deriveKey"]); - const salt = webcrypto.getRandomValues(new Uint8Array(16)); - const key = await webcrypto.subtle.deriveKey( - { - name: "PBKDF2", - salt, - iterations: iteration, - hash: "SHA-256", - }, - keyMaterial, - { name: "AES-GCM", length: 256 }, - false, - ["encrypt"] - ); - KeyBuffs.set(buffKey, { - key, - salt, - count: KEY_RECYCLE_COUNT, - }); - return [key, salt]; +const noun = ["waterfall", "river", "breeze", "moon", "rain", "wind", "sea", "morning", "snow", "lake", "sunset", "pine", "shadow", "leaf", "dawn", "glitter", "forest", "hill", "cloud", "meadow", "sun", "glade", "bird", "brook", "butterfly", "bush", "dew", "dust", "field", "fire", "flower", "firefly", "feather", "grass", "haze", "mountain", "night", "pond", "darkness", "snowflake", "silence", "sound", "sky", "shape", "surf", "thunder", "violet", "water", "wildflower", "wave", "water", "resonance", "sun", "log", "dream", "cherry", "tree", "fog", "frost", "voice", "paper", "frog", "smoke", "star"]; +const adjectives = ["autumn", "hidden", "bitter", "misty", "silent", "empty", "dry", "dark", "summer", "icy", "delicate", "quiet", "white", "cool", "spring", "winter", "patient", "twilight", "dawn", "crimson", "wispy", "weathered", "blue", "billowing", "broken", "cold", "damp", "falling", "frosty", "green", "long", "late", "lingering", "bold", "little", "morning", "muddy", "old", "red", "rough", "still", "small", "sparkling", "thrumming", "shy", "wandering", "withered", "wild", "black", "young", "holy", "solitary", "fragrant", "aged", "snowy", "proud", "floral", "restless", "divine", "polished", "ancient", "purple", "lively", "nameless"]; +function friendlyString() { + return `${adjectives[Math.floor(Math.random() * adjectives.length)]}-${noun[Math.floor(Math.random() * noun.length)]}`; } -function getSemiStaticField(reset?: boolean) { - // return fixed field of iv. - if (semiStaticFieldBuffer != null && !reset) { - return semiStaticFieldBuffer; - } - semiStaticFieldBuffer = webcrypto.getRandomValues(new Uint8Array(12)); - return semiStaticFieldBuffer; -} +const uri_passphrase = `${Deno.env.get("uri_passphrase") ?? friendlyString()}`; -function getNonce() { - // This is nonce, so do not send same thing. - nonceBuffer[0]++; - if (nonceBuffer[0] > 10000) { - // reset semi-static field. - getSemiStaticField(true); - } - return nonceBuffer; -} -function arrayBufferToBase64internalBrowser(buffer: DataView | Uint8Array): Promise { - return new Promise((res, rej) => { - const blob = new Blob([buffer], { type: "application/octet-binary" }); - const reader = new FileReader(); - reader.onload = function (evt) { - const dataURI = evt.target?.result?.toString() || ""; - if (buffer.byteLength != 0 && (dataURI == "" || dataURI == "data:")) return rej(new TypeError("Could not parse the encoded string")); - const result = dataURI.substring(dataURI.indexOf(",") + 1); - res(result); - }; - reader.readAsDataURL(blob); - }); -} - -// Map for converting hexString -const revMap: { [key: string]: number } = {}; -const numMap: { [key: number]: string } = {}; -for (let i = 0; i < 256; i++) { - revMap[(`00${i.toString(16)}`.slice(-2))] = i; - numMap[i] = (`00${i.toString(16)}`.slice(-2)); -} - - -function uint8ArrayToHexString(src: Uint8Array): string { - return [...src].map(e => numMap[e]).join(""); -} - -const QUANTUM = 32768; -async function arrayBufferToBase64Single(buffer: ArrayBuffer): Promise { - const buf = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); - if (buf.byteLength < QUANTUM) return btoa(String.fromCharCode.apply(null, [...buf])); - return await arrayBufferToBase64internalBrowser(buf); -} - - -export async function encrypt(input: string, passphrase: string, autoCalculateIterations: boolean) { - const [key, salt] = await getKeyForEncrypt(passphrase, autoCalculateIterations); - // Create initial vector with semi-fixed part and incremental part - // I think it's not good against related-key attacks. - const fixedPart = getSemiStaticField(); - const invocationPart = getNonce(); - const iv = new Uint8Array([...fixedPart, ...new Uint8Array(invocationPart.buffer)]); - const plainStringified = JSON.stringify(input); - - // const plainStringBuffer: Uint8Array = tex.encode(plainStringified) - const plainStringBuffer: Uint8Array = writeString(plainStringified); - const encryptedDataArrayBuffer = await webcrypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plainStringBuffer); - const encryptedData2 = (await arrayBufferToBase64Single(encryptedDataArrayBuffer)); - //return data with iv and salt. - const ret = `["${encryptedData2}","${uint8ArrayToHexString(iv)}","${uint8ArrayToHexString(salt)}"]`; - return ret; -} const URIBASE = "obsidian://setuplivesync?settings="; async function main() { @@ -173,8 +33,10 @@ async function main() { "concurrencyOfReadChunksOnline": 100, "minimumIntervalOfReadChunksOnline": 100, } - const encryptedConf = encodeURIComponent(await encrypt(JSON.stringify(conf), "welcome", false)); + const encryptedConf = encodeURIComponent(await encrypt(JSON.stringify(conf), uri_passphrase, false)); const theURI = `${URIBASE}${encryptedConf}`; console.log(theURI); + console.log("\nYour passphrase of Setup-URI is: ", uri_passphrase); + console.log("This passphrase is never shown again, so please note it in a safe place.") } await main(); \ No newline at end of file diff --git a/utils/readme.md b/utils/readme.md index 6cc5540..6fe52ac 100644 --- a/utils/readme.md +++ b/utils/readme.md @@ -129,12 +129,15 @@ curl: (35) OpenSSL SSL_connect: Connection reset by peer in connection to young- <-- Configuring CouchDB by REST APIs Done! OK! Setup finished! Also, we can set up Self-hosted LiveSync instantly, by the following setup uri. -Passphrase of setup-uri is `welcome`. +Passphrase of setup-uri will be printed only one time. Keep it safe! --- configured --- database : obsidiannotes E2EE passphrase: dark-wildflower-26467 --- setup uri --- obsidian://setuplivesync?settings=%5B%22gZkBwjFbLqxbdSIbJymU%2FmTPBPAKUiHVGDRKYiNnKhW0auQeBgJOfvnxexZtMCn8sNiIUTAlxNaMGF2t%2BCEhpJoeCP%2FO%2BrwfN5LaNDQyky1Uf7E%2B64A5UWyjOYvZDOgq4iCKSdBAXp9oO%2BwKh4MQjUZ78vIVvJp8Mo6NWHfm5fkiWoAoddki1xBMvi%2BmmN%2FhZatQGcslVb9oyYWpZocduTl0a5Dv%2FQviGwlYQ%2F4NY0dVDIoOdvaYS%2FX4GhNAnLzyJKMXhPEJHo9FvR%2FEOBuwyfMdftV1SQUZ8YDCuiR3T7fh7Kn1c6OFgaFMpFm%2BWgIJ%2FZpmAyhZFpEcjpd7ty%2BN9kfd9gQsZM4%2BYyU9OwDd2DahVMBWkqoV12QIJ8OlJScHHdcUfMW5ex%2F4UZTWKNEHJsigITXBrtq11qGk3rBfHys8O0vY6sz%2FaYNM3iAOsR1aoZGyvwZm4O6VwtzK8edg0T15TL4O%2B7UajQgtCGxgKNYxb8EMOGeskv7NifYhjCWcveeTYOJzBhnIDyRbYaWbkAXQgHPBxzJRkkG%2FpBPfBBoJarj7wgjMvhLJ9xtL4FbP6sBNlr8jtAUCoq4L7LJcRNF4hlgvjJpL2BpFZMzkRNtUBcsRYR5J%2BM1X2buWi2BHncbSiRRDKEwNOQkc%2FmhMJjbAn%2F8eNKRuIICOLD5OvxD7FZNCJ0R%2BWzgrzcNV%22%2C%22ec7edc900516b4fcedb4c7cc01000000%22%2C%22fceb5fe54f6619ee266ed9a887634e07%22%5D + +Your passphrase of Setup-URI is: patient-haze +This passphrase is never shown again, so please note it in a safe place. ``` All we have to do is copy the setup-URI (`obsidian`://...`) and open it from Self-hosted LiveSync on Obsidian.