diff --git a/file.go b/file.go index 66e07e8..6dd1c75 100644 --- a/file.go +++ b/file.go @@ -129,6 +129,43 @@ func FileCountForUser(userID int) (count int, err error) { // {{{ err = row.Scan(&count) return } // }}} +func FileServerUUIDsForUser(userID, page int) (uuids []string, more bool, err error) { // {{{ + uuids = []string{} + + var rows *sqlx.Rows + rows, err = db.Queryx(` + SELECT + uuid + FROM public.file + WHERE + user_id = $1 + ORDER BY + uuid ASC + LIMIT $2 OFFSET $3 + `, + userID, + SYNC_PAGINATION+1, + page*SYNC_PAGINATION, + ) + if err != nil { + return + } + defer rows.Close() + + var uuid string + for rows.Next() { + err = rows.Scan(&uuid) + if err != nil { + return + } + uuids = append(uuids, uuid) + } + + more = len(uuids) > SYNC_PAGINATION + uuids = uuids[0:min(SYNC_PAGINATION, len(uuids))] + + return +} // }}} func FileMetadata(userID int, uuid string) (f File, err error) { // {{{ row := db.QueryRowx(`SELECT * FROM public.file WHERE user_id = $1 AND "uuid" = $2`, userID, uuid) err = row.StructScan(&f) diff --git a/main.go b/main.go index 50d217f..5f68587 100644 --- a/main.go +++ b/main.go @@ -149,8 +149,9 @@ func main() { // {{{ http.HandleFunc("/node/history/count/{uuid}", authenticated(actionNodeHistoryCount)) http.HandleFunc("/file/count", authenticated(actionFileCount)) - http.HandleFunc("/file/{uuid}", authenticated(actionFile)) - http.HandleFunc("/file/{uuid}/metadata", authenticated(actionFileMetadata)) + http.HandleFunc("/file/server_uuids/{offset}", authenticated(actionFileServerUUIDs)) + http.HandleFunc("/file/uuid/{uuid}", authenticated(actionFile)) + http.HandleFunc("/file/uuid/{uuid}/metadata", authenticated(actionFileMetadata)) http.HandleFunc("/service_worker.js", pageServiceWorker) @@ -515,6 +516,22 @@ func actionFileCount(w http.ResponseWriter, r *http.Request) {// {{{ "Count": count, }) }// }}} +func actionFileServerUUIDs(w http.ResponseWriter, r *http.Request) {// {{{ + session := getUserSession(r) + offset, _ := strconv.Atoi(r.PathValue("offset")) + uuids, more, err := FileServerUUIDsForUser(session.UserID, offset) + if err != nil { + Log.Error("file_server_uuids", "error", err) + httpError(w, err) + return + } + + responseData(w, map[string]any{ + "OK": true, + "UUIDs": uuids, + "MoreRowsExist": more, + }) +}// }}} func actionFile(w http.ResponseWriter, r *http.Request) {// {{{ uuid := r.PathValue("uuid") session := getUserSession(r) diff --git a/node.go b/node.go index a25c771..15f277b 100644 --- a/node.go +++ b/node.go @@ -57,67 +57,6 @@ type Node struct { Special bool } -func NodeTree(userID, offset int, synced uint64) (nodes []TreeNode, maxSeq uint64, moreRowsExist bool, err error) { // {{{ - const LIMIT = 100 - var rows *sqlx.Rows - rows, err = db.Queryx(` - SELECT - uuid, - COALESCE(parent_uuid, '') AS parent_uuid, - name, - created, - updated, - deleted IS NOT NULL AS deleted, - created_seq, - updated_seq, - deleted_seq - FROM - public.node - WHERE - user_id = $1 AND - ( - created_seq > $4 OR - updated_seq > $4 OR - deleted_seq > $4 - ) - ORDER BY - created ASC - LIMIT $2 OFFSET $3 - `, - userID, - LIMIT+1, - offset, - synced, - ) - if err != nil { - return - } - defer rows.Close() - - nodes = []TreeNode{} - numNodes := 0 - for rows.Next() { - // Query selects up to one more row than the decided limit. - // Saves one SQL query for row counting. - // Thus if numNodes is larger than the limit, more rows exist for the next call. - numNodes++ - if numNodes > LIMIT { - moreRowsExist = true - return - } - - node := TreeNode{} - if err = rows.StructScan(&node); err != nil { - return - } - nodes = append(nodes, node) - - // DeletedSeq will be 0 if invalid, and thus not be a problem for the max function. - maxSeq = max(maxSeq, node.CreatedSeq, node.UpdatedSeq, uint64(node.DeletedSeq.Int64)) - } - - return -} // }}} func Nodes(userID, offset int, synced uint64, clientUUID string) (nodes []Node, maxSeq uint64, moreRowsExist bool, err error) { // {{{ var rows *sqlx.Rows rows, err = db.Queryx(` diff --git a/static/js/api.mjs b/static/js/api.mjs index 01005e2..9b53cbf 100644 --- a/static/js/api.mjs +++ b/static/js/api.mjs @@ -23,7 +23,6 @@ export class API { throw new Error(json.Error, { cause: { type: 'application', application: json, } }) return json - } catch (err) { // Catch any other errors from fetch. throw new Error(err.message, { cause: { type: 'http', error: err, } }) @@ -31,21 +30,21 @@ export class API { } // Sends a block of binary data to server. - static async upload(uuid, data, sentBytes, file) { + static async upload(uuid, data, sentBytes, metadata) { try { const path = `/sync/file/upload/${uuid}` const headers = { 'X-File-Sent-Bytes': sentBytes, - 'X-File-Total-Bytes': file.size, + 'X-File-Total-Bytes': metadata.size, } // Metadata is needed for the database. // No need to send it every block. if (sentBytes === 0) { - headers['X-File-Name'] = file.name - headers['X-File-Size'] = file.size - headers['X-File-Type'] = file.type - headers['X-File-Modified'] = file.lastModified + headers['X-File-Name'] = metadata.name + headers['X-File-Size'] = metadata.size + headers['X-File-Type'] = metadata.type + headers['X-File-Modified'] = metadata.modified } // Authentication is done with a bearer token. diff --git a/static/js/app.mjs b/static/js/app.mjs index 355c67d..e44eabd 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -1,7 +1,6 @@ -import { ROOT_NODE } from 'node_store' +import { ROOT_NODE, Node } from 'node_store' import { CustomHTMLElement } from './lib/custom_html_element.mjs' import { N2Sidebar } from 'sidebar' -import { Node } from 'node' import { N2PreferenceSet } from './page_preferences.mjs' export class App { @@ -68,8 +67,8 @@ export class App { this.preferences = this.getPreferences() }) - window.addEventListener('keydown', event => this.keyHandler(event)) - window.addEventListener('popstate', event => this.popState(event)) + globalThis.addEventListener('keydown', event => this.keyHandler(event)) + globalThis.addEventListener('popstate', event => this.popState(event)) document.getElementById('notes2').addEventListener('click', event => { if (event.target.id === 'notes2') document.getElementById('node-content')?.focus() @@ -80,12 +79,12 @@ export class App { _mbus.dispatch('SHOW_PAGE', { page: 'node' }) - window._sync = new Sync() + globalThis._sync = new Sync() // I think it is uncomfortable having the sync running as soon as the page load. // I haven't gotten the time to look at the page before stuff jumps around. // There a slight delay to initiate sync seems reasonable. - setTimeout(() => window._sync.run(), 1000) + setTimeout(() => globalThis._sync.run(), 1000) }// }}} keyHandler(event) {//{{{ let handled = true diff --git a/static/js/crypto.mjs b/static/js/crypto.mjs deleted file mode 100644 index 9aab255..0000000 --- a/static/js/crypto.mjs +++ /dev/null @@ -1,72 +0,0 @@ -export default class Crypto { - constructor(key) {//{{{ - if(key === null) - throw new Error("No key provided") - - if(typeof key === 'string') - this.key = sjcl.codec.base64.toBits(base64_key) - else - this.key = key - - this.aes = new sjcl.cipher.aes(this.key) - }//}}} - - static generate_key() {//{{{ - return sjcl.random.randomWords(8) - }//}}} - static pass_to_key(pass, salt = null) {//{{{ - if(salt === null) - salt = sjcl.random.randomWords(4) // 128 bits (16 bytes) - let key = sjcl.misc.pbkdf2(pass, salt, 10000) - - return { - salt, - key, - } - }//}}} - - encrypt(plaintext_data_in_bits, counter, return_encoded = true) {//{{{ - // 8 bytes of random data, (1 word = 4 bytes) * 2 - // with 8 bytes of byte encoded counter is used as - // IV to guarantee a non-repeated IV (which is a catastrophe). - // Assumes counter value is kept unique. Counter is taken from - // Postgres sequence. - let random_bits = sjcl.random.randomWords(2) - let iv_bytes = sjcl.codec.bytes.fromBits(random_bits) - for (let i = 0; i < 8; ++i) { - let mask = 0xffn << BigInt(i*8) - let counter_i_byte = (counter & mask) >> BigInt(i*8) - iv_bytes[15-i] = Number(counter_i_byte) - } - let iv = sjcl.codec.bytes.toBits(iv_bytes) - - let encrypted = sjcl.mode['ccm'].encrypt( - this.aes, - plaintext_data_in_bits, - iv, - ) - - // Returning 16 bytes (4 words) IV + encrypted data. - if(return_encoded) - return sjcl.codec.base64.fromBits( - iv.concat(encrypted) - ) - else - return iv.concat(encrypted) - }//}}} - decrypt(encrypted_base64_data) {//{{{ - try { - let encoded = sjcl.codec.base64.toBits(encrypted_base64_data) - let iv = encoded.slice(0, 4) // in words (4 bytes), not bytes - let encrypted_data = encoded.slice(4) - return sjcl.mode['ccm'].decrypt(this.aes, encrypted_data, iv) - } catch(err) { - if(err.message == `ccm: tag doesn't match`) - throw('Decryption failed') - else - throw(err) - } - }//}}} -} - -// vim: foldmethod=marker diff --git a/static/js/file.mjs b/static/js/file.mjs index cb63f56..8e3bb94 100644 --- a/static/js/file.mjs +++ b/static/js/file.mjs @@ -35,7 +35,7 @@ export class N2File extends CustomHTMLElement { constructor() {// {{{ super(true) - this.file = null + this.streamLocalFile = false this.uuid = null this.addEventListener('click', event => { @@ -54,35 +54,32 @@ export class N2File extends CustomHTMLElement { this.render() }// }}} - openInBrowser(event) { - // this.file is only defined when file is stored in IndexedDB. - if (this.file) + openInBrowser(event) {// {{{ + if (this.streamLocalFile) window.open( - URL.createObjectURL(this.file), + `/stream-local?uuid=${this.metadata.UUID}`, event.ctrlKey ? '_blank' : '_self', ) else { const jwt = localStorage.getItem('token') window.open( - - `/file/${this.uuid}?jwt=${jwt}`, + `/file/uuid/${this.uuid}?jwt=${jwt}`, event.ctrlKey ? '_blank' : '_self', ) } - } + }// }}} async downloadToDisk() {// {{{ try { - // this.file is only defined when file is stored in IndexedDB. // TODO: implement download of remote content. - if (this.file === null) + if (this.streamLocalFile === false) throw new Error('This file is only available online. Download not implemented yet.') const handle = await window.showSaveFilePicker({ - suggestedName: this.file.name, + suggestedName: this.metadata.name, }) const writable = await handle.createWritable() - const blobStream = this.file.stream() + const blobStream = await globalThis.nodeStore.fileSegments.getStream(this.uuid) await blobStream.pipeTo(writable) } catch (err) { if (err.name == 'AbortError') @@ -101,18 +98,21 @@ export class N2File extends CustomHTMLElement { // populateImg makes sure this returned img element exists and then populates it // with the image from IndexedDB. const fileUUID = src.slice(5) - this.uuid = fileUUID - const file = await globalThis.nodeStore.files.get(fileUUID) - if (file) { - this.renderLocalFile(file) + this.uuid = fileUUID + const md = await globalThis.nodeStore.filesMetadata.get(fileUUID) + + if (md) { + this.metadata = md + this.streamLocalFile = true + this.renderLocalFile(md) } else { /* can be undefined when a node is retrieved but files aren't synced. * Try to show from server. * * Metadata is needed to know whether to display an image or the file representation. */ try { - const res = await API.query('GET', `/file/${fileUUID}/metadata`) + const res = await API.query('GET', `/file/uuid/${fileUUID}/metadata`) this.renderRemoteFile(res.Metadata) } catch (err) { console.error(err) @@ -123,27 +123,25 @@ export class N2File extends CustomHTMLElement { // Probably an external URL. this.elImage.src = src }// }}} - async renderLocalFile(file) {// {{{ - this.file = file.file - - if (file.file.type.startsWith('image/')) - this.elImage.src = URL.createObjectURL(file.file) + async renderLocalFile(fileMetadata) {// {{{ + if (fileMetadata.type.startsWith('image/')) + this.elImage.src = `/stream-local?uuid=${fileMetadata.UUID}` else { // Check for and use an existing MIME type icon. // Place them in static/images/file_icons/ and replace the slash with an underscore. - const url = `/images/${_VERSION}/file_icons/${file.file.type.replaceAll('/', '_')}.svg` + const url = `/images/${_VERSION}/file_icons/${fileMetadata.type.replaceAll('/', '_')}.svg` const res = await fetch(url) if (res.ok) this.elImage.src = url - this.elFilename.innerText = file.file.name + this.elFilename.innerText = fileMetadata.name this.elFilename.style.display = 'block' } }// }}} async renderRemoteFile(md) {// {{{ if (md.Type.startsWith('image/')) { const jwt = localStorage.getItem('token') - this.elImage.src = `/file/${md.UUID}?jwt=${jwt}` + this.elImage.src = `/file/uuid/${md.UUID}?jwt=${jwt}` } else { // Check for and use an existing MIME type icon. // Place them in static/images/file_icons/ and replace the slash with an underscore. diff --git a/static/js/key.mjs b/static/js/key.mjs deleted file mode 100644 index 1f8c40f..0000000 --- a/static/js/key.mjs +++ /dev/null @@ -1,240 +0,0 @@ -import { h, Component } from 'preact' -import htm from 'htm' -import Crypto from 'crypto' -const html = htm.bind(h) - -export class Keys extends Component { - constructor(props) {//{{{ - super(props) - this.state = { - create: false, - } - - props.nodeui.retrieveKeys() - }//}}} - render({ nodeui }, { create }) {//{{{ - let keys = nodeui.keys.value - .sort((a,b)=>{ - if(a.description < b.description) return -1 - if(a.description > b.description) return 1 - return 0 - }) - .map(key=> - html`<${KeyComponent} key=${`key-${key.ID}`} model=${key} />` - ) - - let createButton = '' - let createComponents = '' - if(create) { - createComponents = html` -
-

New key

- -
- - - - - - - -
- - -
-
-
- ` - } else { - createButton = html`
` - } - - return html` -
-

Encryption keys

-

- Unlock a key by clicking its name. Lock it by clicking it again. -

- -

- Copy the key and store it in a very secure place to have a way to access notes - in case the password is forgotten, or database is corrupted. -

- -

Click "View key" after unlocking it.

- - ${createButton} - ${createComponents} - -

Keys

-
- ${keys} -
-
` - }//}}} - - generateKey() {//{{{ - let keyTextarea = document.getElementById('key-key') - let key = sjcl.codec.hex.fromBits(Crypto.generate_key()).replace(/(....)/g, '$1 ').trim() - keyTextarea.value = key - }//}}} - validateNewKey() {//{{{ - let keyDescription = document.getElementById('key-description').value - let keyTextarea = document.getElementById('key-key').value - let pass1 = document.getElementById('key-pass1').value - let pass2 = document.getElementById('key-pass2').value - - if(keyDescription.trim() == '') - throw new Error('The key has to have a description') - - if(pass1.trim() == '' || pass1.length < 4) - throw new Error('The password has to be at least 4 characters long.') - - if(pass1 != pass2) - throw new Error(`Passwords doesn't match`) - - let cleanKey = keyTextarea.replace(/\s+/g, '') - if(!cleanKey.match(/^[0-9a-f]{64}$/i)) - throw new Error('Invalid key - has to be 64 characters of 0-9 and A-F') - }//}}} - createKey() {//{{{ - try { - this.validateNewKey() - - let description = document.getElementById('key-description').value - let keyAscii = document.getElementById('key-key').value - let pass1 = document.getElementById('key-pass1').value - - // Key in hex taken from user. - let actual_key = sjcl.codec.hex.toBits(keyAscii.replace(/\s+/g, '')) - - // Key generated from password, used to encrypt the actual key. - let pass_gen = Crypto.pass_to_key(pass1) - - let crypto = new Crypto(pass_gen.key) - let encrypted_actual_key = crypto.encrypt(actual_key, 0x1n, false) - - // Database value is salt + actual key, needed to generate the same key from the password. - let db_encoded = sjcl.codec.hex.fromBits( - pass_gen.salt.concat(encrypted_actual_key) - ) - - // Create on server. - window._app.current.request('/key/create', { - description, - key: db_encoded, - }) - .then(res=>{ - let key = new Key(res.Key, this.props.nodeui.keyCounter) - this.props.nodeui.keys.value = this.props.nodeui.keys.value.concat(key) - }) - .catch(window._app.current.responseError) - } catch(err) { - alert(err.message) - return - } - }//}}} -} - -export class Key { - constructor(data, counter_callback) {//{{{ - this.ID = data.ID - this.description = data.Description - this.encryptedKey = data.Key - this.key = null - - this._counter_cbk = counter_callback - - let hex_key = window.sessionStorage.getItem(`key-${this.ID}`) - if(hex_key) - this.key = sjcl.codec.hex.toBits(hex_key) - }//}}} - status() {//{{{ - if(this.key === null) - return 'locked' - return 'unlocked' - }//}}} - lock() {//{{{ - this.key = null - window.sessionStorage.removeItem(`key-${this.ID}`) - }//}}} - unlock(password) {//{{{ - let db = sjcl.codec.hex.toBits(this.encryptedKey) - let salt = db.slice(0, 4) - let pass_key = Crypto.pass_to_key(password, salt) - let crypto = new Crypto(pass_key.key) - this.key = crypto.decrypt(sjcl.codec.base64.fromBits(db.slice(4))) - window.sessionStorage.setItem(`key-${this.ID}`, sjcl.codec.hex.fromBits(this.key)) - }//}}} - async counter() {//{{{ - return this._counter_cbk() - }//}}} -} - -export class KeyComponent extends Component { - constructor({ model }) {//{{{ - super({ model }) - this.state = { - show_key: false, - } - }//}}} - render({ model }, { show_key }) {//{{{ - let status = '' - switch(model.status()) { - case 'locked': - status = html`
` - break - - case 'unlocked': - status = html`
` - break - } - - let hex_key = '' - if(show_key) { - if(model.status() == 'locked') - hex_key = html`
Unlock key first
` - else { - let key = sjcl.codec.hex.fromBits(model.key) - key = key.replace(/(....)/g, "$1 ").trim() - hex_key = html`
${key}
` - } - } - - let unlocked = model.status()=='unlocked' - - return html` -
this.toggle()}>${status}
-
this.toggle()}>${model.description}
-
this.toggleViewKey()}>${unlocked ? 'View key' : ''}
- ${hex_key} - ` - }//}}} - toggle() {//{{{ - if(this.props.model.status() == 'locked') - this.unlock() - else - this.lock() - }//}}} - lock() {//{{{ - this.props.model.lock() - this.forceUpdate() - }//}}} - unlock() {//{{{ - let pass = prompt("Password") - if(!pass) - return - - try { - this.props.model.unlock(pass) - this.forceUpdate() - } catch(err) { - alert(err) - } - }//}}} - toggleViewKey() {//{{{ - this.setState({ show_key: !this.state.show_key }) - }//}}} -} - -// vim: foldmethod=marker diff --git a/static/js/lib/sjcl.js b/static/js/lib/sjcl.js deleted file mode 100644 index b5a1021..0000000 --- a/static/js/lib/sjcl.js +++ /dev/null @@ -1,2574 +0,0 @@ -/** @fileOverview Javascript cryptography implementation. - * - * Crush to remove comments, shorten variable names and - * generally reduce transmission size. - * - * @author Emily Stark - * @author Mike Hamburg - * @author Dan Boneh - */ - -"use strict"; -/*jslint indent: 2, bitwise: false, nomen: false, plusplus: false, white: false, regexp: false */ -/*global document, window, escape, unescape, module, require, Uint32Array */ - -/** - * The Stanford Javascript Crypto Library, top-level namespace. - * @namespace - */ -var sjcl = { - /** - * Symmetric ciphers. - * @namespace - */ - cipher: {}, - - /** - * Hash functions. Right now only SHA256 is implemented. - * @namespace - */ - hash: {}, - - /** - * Key exchange functions. Right now only SRP is implemented. - * @namespace - */ - keyexchange: {}, - - /** - * Cipher modes of operation. - * @namespace - */ - mode: {}, - - /** - * Miscellaneous. HMAC and PBKDF2. - * @namespace - */ - misc: {}, - - /** - * Bit array encoders and decoders. - * @namespace - * - * @description - * The members of this namespace are functions which translate between - * SJCL's bitArrays and other objects (usually strings). Because it - * isn't always clear which direction is encoding and which is decoding, - * the method names are "fromBits" and "toBits". - */ - codec: {}, - - /** - * Exceptions. - * @namespace - */ - exception: { - /** - * Ciphertext is corrupt. - * @constructor - */ - corrupt: function(message) { - this.toString = function() { return "CORRUPT: "+this.message; }; - this.message = message; - }, - - /** - * Invalid parameter. - * @constructor - */ - invalid: function(message) { - this.toString = function() { return "INVALID: "+this.message; }; - this.message = message; - }, - - /** - * Bug or missing feature in SJCL. - * @constructor - */ - bug: function(message) { - this.toString = function() { return "BUG: "+this.message; }; - this.message = message; - }, - - /** - * Something isn't ready. - * @constructor - */ - notReady: function(message) { - this.toString = function() { return "NOT READY: "+this.message; }; - this.message = message; - } - } -}; -/** @fileOverview Low-level AES implementation. - * - * This file contains a low-level implementation of AES, optimized for - * size and for efficiency on several browsers. It is based on - * OpenSSL's aes_core.c, a public-domain implementation by Vincent - * Rijmen, Antoon Bosselaers and Paulo Barreto. - * - * An older version of this implementation is available in the public - * domain, but this one is (c) Emily Stark, Mike Hamburg, Dan Boneh, - * Stanford University 2008-2010 and BSD-licensed for liability - * reasons. - * - * @author Emily Stark - * @author Mike Hamburg - * @author Dan Boneh - */ - -/** - * Schedule out an AES key for both encryption and decryption. This - * is a low-level class. Use a cipher mode to do bulk encryption. - * - * @constructor - * @param {Array} key The key as an array of 4, 6 or 8 words. - */ -sjcl.cipher.aes = function (key) { - if (!this._tables[0][0][0]) { - this._precompute(); - } - - var i, j, tmp, - encKey, decKey, - sbox = this._tables[0][4], decTable = this._tables[1], - keyLen = key.length, rcon = 1; - - if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) { - throw new sjcl.exception.invalid("invalid aes key size"); - } - - this._key = [encKey = key.slice(0), decKey = []]; - - // schedule encryption keys - for (i = keyLen; i < 4 * keyLen + 28; i++) { - tmp = encKey[i-1]; - - // apply sbox - if (i%keyLen === 0 || (keyLen === 8 && i%keyLen === 4)) { - tmp = sbox[tmp>>>24]<<24 ^ sbox[tmp>>16&255]<<16 ^ sbox[tmp>>8&255]<<8 ^ sbox[tmp&255]; - - // shift rows and add rcon - if (i%keyLen === 0) { - tmp = tmp<<8 ^ tmp>>>24 ^ rcon<<24; - rcon = rcon<<1 ^ (rcon>>7)*283; - } - } - - encKey[i] = encKey[i-keyLen] ^ tmp; - } - - // schedule decryption keys - for (j = 0; i; j++, i--) { - tmp = encKey[j&3 ? i : i - 4]; - if (i<=4 || j<4) { - decKey[j] = tmp; - } else { - decKey[j] = decTable[0][sbox[tmp>>>24 ]] ^ - decTable[1][sbox[tmp>>16 & 255]] ^ - decTable[2][sbox[tmp>>8 & 255]] ^ - decTable[3][sbox[tmp & 255]]; - } - } -}; - -sjcl.cipher.aes.prototype = { - // public - /* Something like this might appear here eventually - name: "AES", - blockSize: 4, - keySizes: [4,6,8], - */ - - /** - * Encrypt an array of 4 big-endian words. - * @param {Array} data The plaintext. - * @return {Array} The ciphertext. - */ - encrypt:function (data) { return this._crypt(data,0); }, - - /** - * Decrypt an array of 4 big-endian words. - * @param {Array} data The ciphertext. - * @return {Array} The plaintext. - */ - decrypt:function (data) { return this._crypt(data,1); }, - - /** - * The expanded S-box and inverse S-box tables. These will be computed - * on the client so that we don't have to send them down the wire. - * - * There are two tables, _tables[0] is for encryption and - * _tables[1] is for decryption. - * - * The first 4 sub-tables are the expanded S-box with MixColumns. The - * last (_tables[01][4]) is the S-box itself. - * - * @private - */ - _tables: [[[],[],[],[],[]],[[],[],[],[],[]]], - - /** - * Expand the S-box tables. - * - * @private - */ - _precompute: function () { - var encTable = this._tables[0], decTable = this._tables[1], - sbox = encTable[4], sboxInv = decTable[4], - i, x, xInv, d=[], th=[], x2, x4, x8, s, tEnc, tDec; - - // Compute double and third tables - for (i = 0; i < 256; i++) { - th[( d[i] = i<<1 ^ (i>>7)*283 )^i]=i; - } - - for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) { - // Compute sbox - s = xInv ^ xInv<<1 ^ xInv<<2 ^ xInv<<3 ^ xInv<<4; - s = s>>8 ^ s&255 ^ 99; - sbox[x] = s; - sboxInv[s] = x; - - // Compute MixColumns - x8 = d[x4 = d[x2 = d[x]]]; - tDec = x8*0x1010101 ^ x4*0x10001 ^ x2*0x101 ^ x*0x1010100; - tEnc = d[s]*0x101 ^ s*0x1010100; - - for (i = 0; i < 4; i++) { - encTable[i][x] = tEnc = tEnc<<24 ^ tEnc>>>8; - decTable[i][s] = tDec = tDec<<24 ^ tDec>>>8; - } - } - - // Compactify. Considerable speedup on Firefox. - for (i = 0; i < 5; i++) { - encTable[i] = encTable[i].slice(0); - decTable[i] = decTable[i].slice(0); - } - }, - - /** - * Encryption and decryption core. - * @param {Array} input Four words to be encrypted or decrypted. - * @param dir The direction, 0 for encrypt and 1 for decrypt. - * @return {Array} The four encrypted or decrypted words. - * @private - */ - _crypt:function (input, dir) { - if (input.length !== 4) { - throw new sjcl.exception.invalid("invalid aes block size"); - } - - var key = this._key[dir], - // state variables a,b,c,d are loaded with pre-whitened data - a = input[0] ^ key[0], - b = input[dir ? 3 : 1] ^ key[1], - c = input[2] ^ key[2], - d = input[dir ? 1 : 3] ^ key[3], - a2, b2, c2, - - nInnerRounds = key.length/4 - 2, - i, - kIndex = 4, - out = [0,0,0,0], - table = this._tables[dir], - - // load up the tables - t0 = table[0], - t1 = table[1], - t2 = table[2], - t3 = table[3], - sbox = table[4]; - - // Inner rounds. Cribbed from OpenSSL. - for (i = 0; i < nInnerRounds; i++) { - a2 = t0[a>>>24] ^ t1[b>>16 & 255] ^ t2[c>>8 & 255] ^ t3[d & 255] ^ key[kIndex]; - b2 = t0[b>>>24] ^ t1[c>>16 & 255] ^ t2[d>>8 & 255] ^ t3[a & 255] ^ key[kIndex + 1]; - c2 = t0[c>>>24] ^ t1[d>>16 & 255] ^ t2[a>>8 & 255] ^ t3[b & 255] ^ key[kIndex + 2]; - d = t0[d>>>24] ^ t1[a>>16 & 255] ^ t2[b>>8 & 255] ^ t3[c & 255] ^ key[kIndex + 3]; - kIndex += 4; - a=a2; b=b2; c=c2; - } - - // Last round. - for (i = 0; i < 4; i++) { - out[dir ? 3&-i : i] = - sbox[a>>>24 ]<<24 ^ - sbox[b>>16 & 255]<<16 ^ - sbox[c>>8 & 255]<<8 ^ - sbox[d & 255] ^ - key[kIndex++]; - a2=a; a=b; b=c; c=d; d=a2; - } - - return out; - } -}; - -/** @fileOverview Arrays of bits, encoded as arrays of Numbers. - * - * @author Emily Stark - * @author Mike Hamburg - * @author Dan Boneh - */ - -/** - * Arrays of bits, encoded as arrays of Numbers. - * @namespace - * @description - *

- * These objects are the currency accepted by SJCL's crypto functions. - *

- * - *

- * Most of our crypto primitives operate on arrays of 4-byte words internally, - * but many of them can take arguments that are not a multiple of 4 bytes. - * This library encodes arrays of bits (whose size need not be a multiple of 8 - * bits) as arrays of 32-bit words. The bits are packed, big-endian, into an - * array of words, 32 bits at a time. Since the words are double-precision - * floating point numbers, they fit some extra data. We use this (in a private, - * possibly-changing manner) to encode the number of bits actually present - * in the last word of the array. - *

- * - *

- * Because bitwise ops clear this out-of-band data, these arrays can be passed - * to ciphers like AES which want arrays of words. - *

- */ -sjcl.bitArray = { - /** - * Array slices in units of bits. - * @param {bitArray} a The array to slice. - * @param {Number} bstart The offset to the start of the slice, in bits. - * @param {Number} bend The offset to the end of the slice, in bits. If this is undefined, - * slice until the end of the array. - * @return {bitArray} The requested slice. - */ - bitSlice: function (a, bstart, bend) { - a = sjcl.bitArray._shiftRight(a.slice(bstart/32), 32 - (bstart & 31)).slice(1); - return (bend === undefined) ? a : sjcl.bitArray.clamp(a, bend-bstart); - }, - - /** - * Extract a number packed into a bit array. - * @param {bitArray} a The array to slice. - * @param {Number} bstart The offset to the start of the slice, in bits. - * @param {Number} blength The length of the number to extract. - * @return {Number} The requested slice. - */ - extract: function(a, bstart, blength) { - // FIXME: this Math.floor is not necessary at all, but for some reason - // seems to suppress a bug in the Chromium JIT. - var x, sh = Math.floor((-bstart-blength) & 31); - if ((bstart + blength - 1 ^ bstart) & -32) { - // it crosses a boundary - x = (a[bstart/32|0] << (32 - sh)) ^ (a[bstart/32+1|0] >>> sh); - } else { - // within a single word - x = a[bstart/32|0] >>> sh; - } - return x & ((1< 0 && len) { - a[l-1] = sjcl.bitArray.partial(len, a[l-1] & 0x80000000 >> (len-1), 1); - } - return a; - }, - - /** - * Make a partial word for a bit array. - * @param {Number} len The number of bits in the word. - * @param {Number} x The bits. - * @param {Number} [_end=0] Pass 1 if x has already been shifted to the high side. - * @return {Number} The partial word. - */ - partial: function (len, x, _end) { - if (len === 32) { return x; } - return (_end ? x|0 : x << (32-len)) + len * 0x10000000000; - }, - - /** - * Get the number of bits used by a partial word. - * @param {Number} x The partial word. - * @return {Number} The number of bits used by the partial word. - */ - getPartial: function (x) { - return Math.round(x/0x10000000000) || 32; - }, - - /** - * Compare two arrays for equality in a predictable amount of time. - * @param {bitArray} a The first array. - * @param {bitArray} b The second array. - * @return {boolean} true if a == b; false otherwise. - */ - equal: function (a, b) { - if (sjcl.bitArray.bitLength(a) !== sjcl.bitArray.bitLength(b)) { - return false; - } - var x = 0, i; - for (i=0; i= 32; shift -= 32) { - out.push(carry); - carry = 0; - } - if (shift === 0) { - return out.concat(a); - } - - for (i=0; i>>shift); - carry = a[i] << (32-shift); - } - last2 = a.length ? a[a.length-1] : 0; - shift2 = sjcl.bitArray.getPartial(last2); - out.push(sjcl.bitArray.partial(shift+shift2 & 31, (shift + shift2 > 32) ? carry : out.pop(),1)); - return out; - }, - - /** xor a block of 4 words together. - * @private - */ - _xor4: function(x,y) { - return [x[0]^y[0],x[1]^y[1],x[2]^y[2],x[3]^y[3]]; - }, - - /** byteswap a word array inplace. - * (does not handle partial words) - * @param {sjcl.bitArray} a word array - * @return {sjcl.bitArray} byteswapped array - */ - byteswapM: function(a) { - var i, v, m = 0xff00; - for (i = 0; i < a.length; ++i) { - v = a[i]; - a[i] = (v >>> 24) | ((v >>> 8) & m) | ((v & m) << 8) | (v << 24); - } - return a; - } -}; -/** @fileOverview Bit array codec implementations. - * - * @author Emily Stark - * @author Mike Hamburg - * @author Dan Boneh - */ - -/** - * UTF-8 strings - * @namespace - */ -sjcl.codec.utf8String = { - /** Convert from a bitArray to a UTF-8 string. */ - fromBits: function (arr) { - var out = "", bl = sjcl.bitArray.bitLength(arr), i, tmp; - for (i=0; i>> 8 >>> 8 >>> 8); - tmp <<= 8; - } - return decodeURIComponent(escape(out)); - }, - - /** Convert from a UTF-8 string to a bitArray. */ - toBits: function (str) { - str = unescape(encodeURIComponent(str)); - var out = [], i, tmp=0; - for (i=0; i>>bits) >>> REMAINING); - if (bits < BASE) { - ta = arr[i] << (BASE-bits); - bits += REMAINING; - i++; - } else { - ta <<= BASE; - bits -= BASE; - } - } - while ((out.length & 7) && !_noEquals) { out += "="; } - - return out; - }, - - /** Convert from a base32 string to a bitArray */ - toBits: function(str, _hex) { - str = str.replace(/\s|=/g,'').toUpperCase(); - var BITS = sjcl.codec.base32.BITS, BASE = sjcl.codec.base32.BASE, REMAINING = sjcl.codec.base32.REMAINING; - var out = [], i, bits=0, c = sjcl.codec.base32._chars, ta=0, x, format="base32"; - - if (_hex) { - c = sjcl.codec.base32._hexChars; - format = "base32hex"; - } - - for (i=0; i REMAINING) { - bits -= REMAINING; - out.push(ta ^ x>>>bits); - ta = x << (BITS-bits); - } else { - bits += BASE; - ta ^= x << (BITS-bits); - } - } - if (bits&56) { - out.push(sjcl.bitArray.partial(bits&56, ta, 1)); - } - return out; - } -}; - -sjcl.codec.base32hex = { - fromBits: function (arr, _noEquals) { return sjcl.codec.base32.fromBits(arr,_noEquals,1); }, - toBits: function (str) { return sjcl.codec.base32.toBits(str,1); } -}; -/** @fileOverview Bit array codec implementations. - * - * @author Emily Stark - * @author Mike Hamburg - * @author Dan Boneh - */ - -/** - * Base64 encoding/decoding - * @namespace - */ -sjcl.codec.base64 = { - /** The base64 alphabet. - * @private - */ - _chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", - - /** Convert from a bitArray to a base64 string. */ - fromBits: function (arr, _noEquals, _url) { - var out = "", i, bits=0, c = sjcl.codec.base64._chars, ta=0, bl = sjcl.bitArray.bitLength(arr); - if (_url) { - c = c.substr(0,62) + '-_'; - } - for (i=0; out.length * 6 < bl; ) { - out += c.charAt((ta ^ arr[i]>>>bits) >>> 26); - if (bits < 6) { - ta = arr[i] << (6-bits); - bits += 26; - i++; - } else { - ta <<= 6; - bits -= 6; - } - } - while ((out.length & 3) && !_noEquals) { out += "="; } - return out; - }, - - /** Convert from a base64 string to a bitArray */ - toBits: function(str, _url) { - str = str.replace(/\s|=/g,''); - var out = [], i, bits=0, c = sjcl.codec.base64._chars, ta=0, x; - if (_url) { - c = c.substr(0,62) + '-_'; - } - for (i=0; i 26) { - bits -= 26; - out.push(ta ^ x>>>bits); - ta = x << (32-bits); - } else { - bits += 6; - ta ^= x << (32-bits); - } - } - if (bits&56) { - out.push(sjcl.bitArray.partial(bits&56, ta, 1)); - } - return out; - } -}; - -sjcl.codec.base64url = { - fromBits: function (arr) { return sjcl.codec.base64.fromBits(arr,1,1); }, - toBits: function (str) { return sjcl.codec.base64.toBits(str,1); } -}; -/** @fileOverview Bit array codec implementations. - * - * @author Emily Stark - * @author Mike Hamburg - * @author Dan Boneh - */ - -/** - * Arrays of bytes - * @namespace - */ -sjcl.codec.bytes = { - /** Convert from a bitArray to an array of bytes. */ - fromBits: function (arr) { - var out = [], bl = sjcl.bitArray.bitLength(arr), i, tmp; - for (i=0; i>> 24); - tmp <<= 8; - } - return out; - }, - /** Convert from an array of bytes to a bitArray. */ - toBits: function (bytes) { - var out = [], i, tmp=0; - for (i=0; i 9007199254740991){ - throw new sjcl.exception.invalid("Cannot hash more than 2^53 - 1 bits"); - } - - if (typeof Uint32Array !== 'undefined') { - var c = new Uint32Array(b); - var j = 0; - for (i = 512+ol - ((512+ol) & 511); i <= nl; i+= 512) { - this._block(c.subarray(16 * j, 16 * (j+1))); - j += 1; - } - b.splice(0, 16 * j); - } else { - for (i = 512+ol - ((512+ol) & 511); i <= nl; i+= 512) { - this._block(b.splice(0,16)); - } - } - return this; - }, - - /** - * Complete hashing and output the hash value. - * @return {bitArray} The hash value, an array of 8 big-endian words. - */ - finalize:function () { - var i, b = this._buffer, h = this._h; - - // Round out and push the buffer - b = sjcl.bitArray.concat(b, [sjcl.bitArray.partial(1,1)]); - - // Round out the buffer to a multiple of 16 words, less the 2 length words. - for (i = b.length + 2; i & 15; i++) { - b.push(0); - } - - // append the length - b.push(Math.floor(this._length / 0x100000000)); - b.push(this._length | 0); - - while (b.length) { - this._block(b.splice(0,16)); - } - - this.reset(); - return h; - }, - - /** - * The SHA-256 initialization vector, to be precomputed. - * @private - */ - _init:[], - /* - _init:[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19], - */ - - /** - * The SHA-256 hash key, to be precomputed. - * @private - */ - _key:[], - /* - _key: - [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, - 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, - 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, - 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, - 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, - 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, - 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, - 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2], - */ - - - /** - * Function to precompute _init and _key. - * @private - */ - _precompute: function () { - var i = 0, prime = 2, factor, isPrime; - - function frac(x) { return (x-Math.floor(x)) * 0x100000000 | 0; } - - for (; i<64; prime++) { - isPrime = true; - for (factor=2; factor*factor <= prime; factor++) { - if (prime % factor === 0) { - isPrime = false; - break; - } - } - if (isPrime) { - if (i<8) { - this._init[i] = frac(Math.pow(prime, 1/2)); - } - this._key[i] = frac(Math.pow(prime, 1/3)); - i++; - } - } - }, - - /** - * Perform one cycle of SHA-256. - * @param {Uint32Array|bitArray} w one block of words. - * @private - */ - _block:function (w) { - var i, tmp, a, b, - h = this._h, - k = this._key, - h0 = h[0], h1 = h[1], h2 = h[2], h3 = h[3], - h4 = h[4], h5 = h[5], h6 = h[6], h7 = h[7]; - - /* Rationale for placement of |0 : - * If a value can overflow is original 32 bits by a factor of more than a few - * million (2^23 ish), there is a possibility that it might overflow the - * 53-bit mantissa and lose precision. - * - * To avoid this, we clamp back to 32 bits by |'ing with 0 on any value that - * propagates around the loop, and on the hash state h[]. I don't believe - * that the clamps on h4 and on h0 are strictly necessary, but it's close - * (for h4 anyway), and better safe than sorry. - * - * The clamps on h[] are necessary for the output to be correct even in the - * common case and for short inputs. - */ - for (i=0; i<64; i++) { - // load up the input word for this round - if (i<16) { - tmp = w[i]; - } else { - a = w[(i+1 ) & 15]; - b = w[(i+14) & 15]; - tmp = w[i&15] = ((a>>>7 ^ a>>>18 ^ a>>>3 ^ a<<25 ^ a<<14) + - (b>>>17 ^ b>>>19 ^ b>>>10 ^ b<<15 ^ b<<13) + - w[i&15] + w[(i+9) & 15]) | 0; - } - - tmp = (tmp + h7 + (h4>>>6 ^ h4>>>11 ^ h4>>>25 ^ h4<<26 ^ h4<<21 ^ h4<<7) + (h6 ^ h4&(h5^h6)) + k[i]); // | 0; - - // shift register - h7 = h6; h6 = h5; h5 = h4; - h4 = h3 + tmp | 0; - h3 = h2; h2 = h1; h1 = h0; - - h0 = (tmp + ((h1&h2) ^ (h3&(h1^h2))) + (h1>>>2 ^ h1>>>13 ^ h1>>>22 ^ h1<<30 ^ h1<<19 ^ h1<<10)) | 0; - } - - h[0] = h[0]+h0 | 0; - h[1] = h[1]+h1 | 0; - h[2] = h[2]+h2 | 0; - h[3] = h[3]+h3 | 0; - h[4] = h[4]+h4 | 0; - h[5] = h[5]+h5 | 0; - h[6] = h[6]+h6 | 0; - h[7] = h[7]+h7 | 0; - } -}; - - -/** @fileOverview CCM mode implementation. - * - * Special thanks to Roy Nicholson for pointing out a bug in our - * implementation. - * - * @author Emily Stark - * @author Mike Hamburg - * @author Dan Boneh - */ - -/** - * CTR mode with CBC MAC. - * @namespace - */ -sjcl.mode.ccm = { - /** The name of the mode. - * @constant - */ - name: "ccm", - - _progressListeners: [], - - listenProgress: function (cb) { - sjcl.mode.ccm._progressListeners.push(cb); - }, - - unListenProgress: function (cb) { - var index = sjcl.mode.ccm._progressListeners.indexOf(cb); - if (index > -1) { - sjcl.mode.ccm._progressListeners.splice(index, 1); - } - }, - - _callProgressListener: function (val) { - var p = sjcl.mode.ccm._progressListeners.slice(), i; - - for (i = 0; i < p.length; i += 1) { - p[i](val); - } - }, - - /** Encrypt in CCM mode. - * @static - * @param {Object} prf The pseudorandom function. It must have a block size of 16 bytes. - * @param {bitArray} plaintext The plaintext data. - * @param {bitArray} iv The initialization value. - * @param {bitArray} [adata=[]] The authenticated data. - * @param {Number} [tlen=64] the desired tag length, in bits. - * @return {bitArray} The encrypted data, an array of bytes. - */ - encrypt: function(prf, plaintext, iv, adata, tlen) { - var L, out = plaintext.slice(0), tag, w=sjcl.bitArray, ivl = w.bitLength(iv) / 8, ol = w.bitLength(out) / 8; - tlen = tlen || 64; - adata = adata || []; - - if (ivl < 7) { - throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes"); - } - - // compute the length of the length - for (L=2; L<4 && ol >>> 8*L; L++) {} - if (L < 15 - ivl) { L = 15-ivl; } - iv = w.clamp(iv,8*(15-L)); - - // compute the tag - tag = sjcl.mode.ccm._computeTag(prf, plaintext, iv, adata, tlen, L); - - // encrypt - out = sjcl.mode.ccm._ctrMode(prf, out, iv, tag, tlen, L); - - return w.concat(out.data, out.tag); - }, - - /** Decrypt in CCM mode. - * @static - * @param {Object} prf The pseudorandom function. It must have a block size of 16 bytes. - * @param {bitArray} ciphertext The ciphertext data. - * @param {bitArray} iv The initialization value. - * @param {bitArray} [adata=[]] adata The authenticated data. - * @param {Number} [tlen=64] tlen the desired tag length, in bits. - * @return {bitArray} The decrypted data. - */ - decrypt: function(prf, ciphertext, iv, adata, tlen) { - tlen = tlen || 64; - adata = adata || []; - var L, - w=sjcl.bitArray, - ivl = w.bitLength(iv) / 8, - ol = w.bitLength(ciphertext), - out = w.clamp(ciphertext, ol - tlen), - tag = w.bitSlice(ciphertext, ol - tlen), tag2; - - - ol = (ol - tlen) / 8; - - if (ivl < 7) { - throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes"); - } - - // compute the length of the length - for (L=2; L<4 && ol >>> 8*L; L++) {} - if (L < 15 - ivl) { L = 15-ivl; } - iv = w.clamp(iv,8*(15-L)); - - // decrypt - out = sjcl.mode.ccm._ctrMode(prf, out, iv, tag, tlen, L); - - // check the tag - tag2 = sjcl.mode.ccm._computeTag(prf, out.data, iv, adata, tlen, L); - if (!w.equal(out.tag, tag2)) { - throw new sjcl.exception.corrupt("ccm: tag doesn't match"); - } - - return out.data; - }, - - _macAdditionalData: function (prf, adata, iv, tlen, ol, L) { - var mac, tmp, i, macData = [], w=sjcl.bitArray, xor = w._xor4; - - // mac the flags - mac = [w.partial(8, (adata.length ? 1<<6 : 0) | (tlen-2) << 2 | L-1)]; - - // mac the iv and length - mac = w.concat(mac, iv); - mac[3] |= ol; - mac = prf.encrypt(mac); - - if (adata.length) { - // mac the associated data. start with its length... - tmp = w.bitLength(adata)/8; - if (tmp <= 0xFEFF) { - macData = [w.partial(16, tmp)]; - } else if (tmp <= 0xFFFFFFFF) { - macData = w.concat([w.partial(16,0xFFFE)], [tmp]); - } // else ... - - // mac the data itself - macData = w.concat(macData, adata); - for (i=0; i 16) { - throw new sjcl.exception.invalid("ccm: invalid tag length"); - } - - if (adata.length > 0xFFFFFFFF || plaintext.length > 0xFFFFFFFF) { - // I don't want to deal with extracting high words from doubles. - throw new sjcl.exception.bug("ccm: can't deal with 4GiB or more data"); - } - - mac = sjcl.mode.ccm._macAdditionalData(prf, adata, iv, tlen, w.bitLength(plaintext)/8, L); - - // mac the plaintext - for (i=0; i n) { - sjcl.mode.ccm._callProgressListener(i/l); - n += p; - } - ctr[3]++; - enc = prf.encrypt(ctr); - data[i] ^= enc[0]; - data[i+1] ^= enc[1]; - data[i+2] ^= enc[2]; - data[i+3] ^= enc[3]; - } - return { tag:tag, data:w.clamp(data,bl) }; - } -}; -/** @fileOverview OCB 2.0 implementation - * - * @author Emily Stark - * @author Mike Hamburg - * @author Dan Boneh - */ - -/** - * Phil Rogaway's Offset CodeBook mode, version 2.0. - * May be covered by US and international patents. - * - * @namespace - * @author Emily Stark - * @author Mike Hamburg - * @author Dan Boneh - */ -sjcl.mode.ocb2 = { - /** The name of the mode. - * @constant - */ - name: "ocb2", - - /** Encrypt in OCB mode, version 2.0. - * @param {Object} prp The block cipher. It must have a block size of 16 bytes. - * @param {bitArray} plaintext The plaintext data. - * @param {bitArray} iv The initialization value. - * @param {bitArray} [adata=[]] The authenticated data. - * @param {Number} [tlen=64] the desired tag length, in bits. - * @param {boolean} [premac=false] true if the authentication data is pre-macced with PMAC. - * @return The encrypted data, an array of bytes. - * @throws {sjcl.exception.invalid} if the IV isn't exactly 128 bits. - */ - encrypt: function(prp, plaintext, iv, adata, tlen, premac) { - if (sjcl.bitArray.bitLength(iv) !== 128) { - throw new sjcl.exception.invalid("ocb iv must be 128 bits"); - } - var i, - times2 = sjcl.mode.ocb2._times2, - w = sjcl.bitArray, - xor = w._xor4, - checksum = [0,0,0,0], - delta = times2(prp.encrypt(iv)), - bi, bl, - output = [], - pad; - - adata = adata || []; - tlen = tlen || 64; - - for (i=0; i+4 < plaintext.length; i+=4) { - /* Encrypt a non-final block */ - bi = plaintext.slice(i,i+4); - checksum = xor(checksum, bi); - output = output.concat(xor(delta,prp.encrypt(xor(delta, bi)))); - delta = times2(delta); - } - - /* Chop out the final block */ - bi = plaintext.slice(i); - bl = w.bitLength(bi); - pad = prp.encrypt(xor(delta,[0,0,0,bl])); - bi = w.clamp(xor(bi.concat([0,0,0]),pad), bl); - - /* Checksum the final block, and finalize the checksum */ - checksum = xor(checksum,xor(bi.concat([0,0,0]),pad)); - checksum = prp.encrypt(xor(checksum,xor(delta,times2(delta)))); - - /* MAC the header */ - if (adata.length) { - checksum = xor(checksum, premac ? adata : sjcl.mode.ocb2.pmac(prp, adata)); - } - - return output.concat(w.concat(bi, w.clamp(checksum, tlen))); - }, - - /** Decrypt in OCB mode. - * @param {Object} prp The block cipher. It must have a block size of 16 bytes. - * @param {bitArray} ciphertext The ciphertext data. - * @param {bitArray} iv The initialization value. - * @param {bitArray} [adata=[]] The authenticated data. - * @param {Number} [tlen=64] the desired tag length, in bits. - * @param {boolean} [premac=false] true if the authentication data is pre-macced with PMAC. - * @return The decrypted data, an array of bytes. - * @throws {sjcl.exception.invalid} if the IV isn't exactly 128 bits. - * @throws {sjcl.exception.corrupt} if if the message is corrupt. - */ - decrypt: function(prp, ciphertext, iv, adata, tlen, premac) { - if (sjcl.bitArray.bitLength(iv) !== 128) { - throw new sjcl.exception.invalid("ocb iv must be 128 bits"); - } - tlen = tlen || 64; - var i, - times2 = sjcl.mode.ocb2._times2, - w = sjcl.bitArray, - xor = w._xor4, - checksum = [0,0,0,0], - delta = times2(prp.encrypt(iv)), - bi, bl, - len = sjcl.bitArray.bitLength(ciphertext) - tlen, - output = [], - pad; - - adata = adata || []; - - for (i=0; i+4 < len/32; i+=4) { - /* Decrypt a non-final block */ - bi = xor(delta, prp.decrypt(xor(delta, ciphertext.slice(i,i+4)))); - checksum = xor(checksum, bi); - output = output.concat(bi); - delta = times2(delta); - } - - /* Chop out and decrypt the final block */ - bl = len-i*32; - pad = prp.encrypt(xor(delta,[0,0,0,bl])); - bi = xor(pad, w.clamp(ciphertext.slice(i),bl).concat([0,0,0])); - - /* Checksum the final block, and finalize the checksum */ - checksum = xor(checksum, bi); - checksum = prp.encrypt(xor(checksum, xor(delta, times2(delta)))); - - /* MAC the header */ - if (adata.length) { - checksum = xor(checksum, premac ? adata : sjcl.mode.ocb2.pmac(prp, adata)); - } - - if (!w.equal(w.clamp(checksum, tlen), w.bitSlice(ciphertext, len))) { - throw new sjcl.exception.corrupt("ocb: tag doesn't match"); - } - - return output.concat(w.clamp(bi,bl)); - }, - - /** PMAC authentication for OCB associated data. - * @param {Object} prp The block cipher. It must have a block size of 16 bytes. - * @param {bitArray} adata The authenticated data. - */ - pmac: function(prp, adata) { - var i, - times2 = sjcl.mode.ocb2._times2, - w = sjcl.bitArray, - xor = w._xor4, - checksum = [0,0,0,0], - delta = prp.encrypt([0,0,0,0]), - bi; - - delta = xor(delta,times2(times2(delta))); - - for (i=0; i+4>>31, - x[1]<<1 ^ x[2]>>>31, - x[2]<<1 ^ x[3]>>>31, - x[3]<<1 ^ (x[0]>>>31)*0x87]; - } -}; -/** @fileOverview GCM mode implementation. - * - * @author Juho Vähä-Herttua - */ - -/** - * Galois/Counter mode. - * @namespace - */ -sjcl.mode.gcm = { - /** - * The name of the mode. - * @constant - */ - name: "gcm", - - /** Encrypt in GCM mode. - * @static - * @param {Object} prf The pseudorandom function. It must have a block size of 16 bytes. - * @param {bitArray} plaintext The plaintext data. - * @param {bitArray} iv The initialization value. - * @param {bitArray} [adata=[]] The authenticated data. - * @param {Number} [tlen=128] The desired tag length, in bits. - * @return {bitArray} The encrypted data, an array of bytes. - */ - encrypt: function (prf, plaintext, iv, adata, tlen) { - var out, data = plaintext.slice(0), w=sjcl.bitArray; - tlen = tlen || 128; - adata = adata || []; - - // encrypt and tag - out = sjcl.mode.gcm._ctrMode(true, prf, data, adata, iv, tlen); - - return w.concat(out.data, out.tag); - }, - - /** Decrypt in GCM mode. - * @static - * @param {Object} prf The pseudorandom function. It must have a block size of 16 bytes. - * @param {bitArray} ciphertext The ciphertext data. - * @param {bitArray} iv The initialization value. - * @param {bitArray} [adata=[]] The authenticated data. - * @param {Number} [tlen=128] The desired tag length, in bits. - * @return {bitArray} The decrypted data. - */ - decrypt: function (prf, ciphertext, iv, adata, tlen) { - var out, data = ciphertext.slice(0), tag, w=sjcl.bitArray, l=w.bitLength(data); - tlen = tlen || 128; - adata = adata || []; - - // Slice tag out of data - if (tlen <= l) { - tag = w.bitSlice(data, l-tlen); - data = w.bitSlice(data, 0, l-tlen); - } else { - tag = data; - data = []; - } - - // decrypt and tag - out = sjcl.mode.gcm._ctrMode(false, prf, data, adata, iv, tlen); - - if (!w.equal(out.tag, tag)) { - throw new sjcl.exception.corrupt("gcm: tag doesn't match"); - } - return out.data; - }, - - /* Compute the galois multiplication of X and Y - * @private - */ - _galoisMultiply: function (x, y) { - var i, j, xi, Zi, Vi, lsb_Vi, w=sjcl.bitArray, xor=w._xor4; - - Zi = [0,0,0,0]; - Vi = y.slice(0); - - // Block size is 128 bits, run 128 times to get Z_128 - for (i=0; i<128; i++) { - xi = (x[Math.floor(i/32)] & (1 << (31-i%32))) !== 0; - if (xi) { - // Z_i+1 = Z_i ^ V_i - Zi = xor(Zi, Vi); - } - - // Store the value of LSB(V_i) - lsb_Vi = (Vi[3] & 1) !== 0; - - // V_i+1 = V_i >> 1 - for (j=3; j>0; j--) { - Vi[j] = (Vi[j] >>> 1) | ((Vi[j-1]&1) << 31); - } - Vi[0] = Vi[0] >>> 1; - - // If LSB(V_i) is 1, V_i+1 = (V_i >> 1) ^ R - if (lsb_Vi) { - Vi[0] = Vi[0] ^ (0xe1 << 24); - } - } - return Zi; - }, - - _ghash: function(H, Y0, data) { - var Yi, i, l = data.length; - - Yi = Y0.slice(0); - for (i=0; i bs) { - key = Hash.hash(key); - } - - for (i=0; iUse sjcl.random as a singleton for this class! - *

- * This random number generator is a derivative of Ferguson and Schneier's - * generator Fortuna. It collects entropy from various events into several - * pools, implemented by streaming SHA-256 instances. It differs from - * ordinary Fortuna in a few ways, though. - *

- * - *

- * Most importantly, it has an entropy estimator. This is present because - * there is a strong conflict here between making the generator available - * as soon as possible, and making sure that it doesn't "run on empty". - * In Fortuna, there is a saved state file, and the system is likely to have - * time to warm up. - *

- * - *

- * Second, because users are unlikely to stay on the page for very long, - * and to speed startup time, the number of pools increases logarithmically: - * a new pool is created when the previous one is actually used for a reseed. - * This gives the same asymptotic guarantees as Fortuna, but gives more - * entropy to early reseeds. - *

- * - *

- * The entire mechanism here feels pretty klunky. Furthermore, there are - * several improvements that should be made, including support for - * dedicated cryptographic functions that may be present in some browsers; - * state files in local storage; cookies containing randomness; etc. So - * look for improvements in future versions. - *

- * @constructor - */ -sjcl.prng = function(defaultParanoia) { - - /* private */ - this._pools = [new sjcl.hash.sha256()]; - this._poolEntropy = [0]; - this._reseedCount = 0; - this._robins = {}; - this._eventId = 0; - - this._collectorIds = {}; - this._collectorIdNext = 0; - - this._strength = 0; - this._poolStrength = 0; - this._nextReseed = 0; - this._key = [0,0,0,0,0,0,0,0]; - this._counter = [0,0,0,0]; - this._cipher = undefined; - this._defaultParanoia = defaultParanoia; - - /* event listener stuff */ - this._collectorsStarted = false; - this._callbacks = {progress: {}, seeded: {}}; - this._callbackI = 0; - - /* constants */ - this._NOT_READY = 0; - this._READY = 1; - this._REQUIRES_RESEED = 2; - - this._MAX_WORDS_PER_BURST = 65536; - this._PARANOIA_LEVELS = [0,48,64,96,128,192,256,384,512,768,1024]; - this._MILLISECONDS_PER_RESEED = 30000; - this._BITS_PER_RESEED = 80; -}; - -sjcl.prng.prototype = { - /** Generate several random words, and return them in an array. - * A word consists of 32 bits (4 bytes) - * @param {Number} nwords The number of words to generate. - */ - randomWords: function (nwords, paranoia) { - var out = [], i, readiness = this.isReady(paranoia), g; - - if (readiness === this._NOT_READY) { - throw new sjcl.exception.notReady("generator isn't seeded"); - } else if (readiness & this._REQUIRES_RESEED) { - this._reseedFromPools(!(readiness & this._READY)); - } - - for (i=0; i0) { - estimatedEntropy++; - tmp = tmp >>> 1; - } - } - } - this._pools[robin].update([id,this._eventId++,2,estimatedEntropy,t,data.length].concat(data)); - } - break; - - case "string": - if (estimatedEntropy === undefined) { - /* English text has just over 1 bit per character of entropy. - * But this might be HTML or something, and have far less - * entropy than English... Oh well, let's just say one bit. - */ - estimatedEntropy = data.length; - } - this._pools[robin].update([id,this._eventId++,3,estimatedEntropy,t,data.length]); - this._pools[robin].update(data); - break; - - default: - err=1; - } - if (err) { - throw new sjcl.exception.bug("random: addEntropy only supports number, array of numbers or string"); - } - - /* record the new strength */ - this._poolEntropy[robin] += estimatedEntropy; - this._poolStrength += estimatedEntropy; - - /* fire off events */ - if (oldReady === this._NOT_READY) { - if (this.isReady() !== this._NOT_READY) { - this._fireEvent("seeded", Math.max(this._strength, this._poolStrength)); - } - this._fireEvent("progress", this.getProgress()); - } - }, - - /** Is the generator ready? */ - isReady: function (paranoia) { - var entropyRequired = this._PARANOIA_LEVELS[ (paranoia !== undefined) ? paranoia : this._defaultParanoia ]; - - if (this._strength && this._strength >= entropyRequired) { - return (this._poolEntropy[0] > this._BITS_PER_RESEED && (new Date()).valueOf() > this._nextReseed) ? - this._REQUIRES_RESEED | this._READY : - this._READY; - } else { - return (this._poolStrength >= entropyRequired) ? - this._REQUIRES_RESEED | this._NOT_READY : - this._NOT_READY; - } - }, - - /** Get the generator's progress toward readiness, as a fraction */ - getProgress: function (paranoia) { - var entropyRequired = this._PARANOIA_LEVELS[ paranoia ? paranoia : this._defaultParanoia ]; - - if (this._strength >= entropyRequired) { - return 1.0; - } else { - return (this._poolStrength > entropyRequired) ? - 1.0 : - this._poolStrength / entropyRequired; - } - }, - - /** start the built-in entropy collectors */ - startCollectors: function () { - if (this._collectorsStarted) { return; } - - this._eventListener = { - loadTimeCollector: this._bind(this._loadTimeCollector), - mouseCollector: this._bind(this._mouseCollector), - keyboardCollector: this._bind(this._keyboardCollector), - accelerometerCollector: this._bind(this._accelerometerCollector), - touchCollector: this._bind(this._touchCollector) - }; - - if (window.addEventListener) { - window.addEventListener("load", this._eventListener.loadTimeCollector, false); - window.addEventListener("mousemove", this._eventListener.mouseCollector, false); - window.addEventListener("keypress", this._eventListener.keyboardCollector, false); - window.addEventListener("devicemotion", this._eventListener.accelerometerCollector, false); - window.addEventListener("touchmove", this._eventListener.touchCollector, false); - } else if (document.attachEvent) { - document.attachEvent("onload", this._eventListener.loadTimeCollector); - document.attachEvent("onmousemove", this._eventListener.mouseCollector); - document.attachEvent("keypress", this._eventListener.keyboardCollector); - } else { - throw new sjcl.exception.bug("can't attach event"); - } - - this._collectorsStarted = true; - }, - - /** stop the built-in entropy collectors */ - stopCollectors: function () { - if (!this._collectorsStarted) { return; } - - if (window.removeEventListener) { - window.removeEventListener("load", this._eventListener.loadTimeCollector, false); - window.removeEventListener("mousemove", this._eventListener.mouseCollector, false); - window.removeEventListener("keypress", this._eventListener.keyboardCollector, false); - window.removeEventListener("devicemotion", this._eventListener.accelerometerCollector, false); - window.removeEventListener("touchmove", this._eventListener.touchCollector, false); - } else if (document.detachEvent) { - document.detachEvent("onload", this._eventListener.loadTimeCollector); - document.detachEvent("onmousemove", this._eventListener.mouseCollector); - document.detachEvent("keypress", this._eventListener.keyboardCollector); - } - - this._collectorsStarted = false; - }, - - /* use a cookie to store entropy. - useCookie: function (all_cookies) { - throw new sjcl.exception.bug("random: useCookie is unimplemented"); - },*/ - - /** add an event listener for progress or seeded-ness. */ - addEventListener: function (name, callback) { - this._callbacks[name][this._callbackI++] = callback; - }, - - /** remove an event listener for progress or seeded-ness */ - removeEventListener: function (name, cb) { - var i, j, cbs=this._callbacks[name], jsTemp=[]; - - /* I'm not sure if this is necessary; in C++, iterating over a - * collection and modifying it at the same time is a no-no. - */ - - for (j in cbs) { - if (cbs.hasOwnProperty(j) && cbs[j] === cb) { - jsTemp.push(j); - } - } - - for (i=0; i= 1 << this._pools.length) { - this._pools.push(new sjcl.hash.sha256()); - this._poolEntropy.push(0); - } - - /* how strong was this reseed? */ - this._poolStrength -= strength; - if (strength > this._strength) { - this._strength = strength; - } - - this._reseedCount ++; - this._reseed(reseedData); - }, - - _keyboardCollector: function () { - this._addCurrentTimeToEntropy(1); - }, - - _mouseCollector: function (ev) { - var x, y; - - try { - x = ev.x || ev.clientX || ev.offsetX || 0; - y = ev.y || ev.clientY || ev.offsetY || 0; - } catch (err) { - // Event originated from a secure element. No mouse position available. - x = 0; - y = 0; - } - - if (x != 0 && y!= 0) { - this.addEntropy([x,y], 2, "mouse"); - } - - this._addCurrentTimeToEntropy(0); - }, - - _touchCollector: function(ev) { - var touch = ev.touches[0] || ev.changedTouches[0]; - var x = touch.pageX || touch.clientX, - y = touch.pageY || touch.clientY; - - this.addEntropy([x,y],1,"touch"); - - this._addCurrentTimeToEntropy(0); - }, - - _loadTimeCollector: function () { - this._addCurrentTimeToEntropy(2); - }, - - _addCurrentTimeToEntropy: function (estimatedEntropy) { - if (typeof window !== 'undefined' && window.performance && typeof window.performance.now === "function") { - //how much entropy do we want to add here? - this.addEntropy(window.performance.now(), estimatedEntropy, "loadtime"); - } else { - this.addEntropy((new Date()).valueOf(), estimatedEntropy, "loadtime"); - } - }, - _accelerometerCollector: function (ev) { - var ac = ev.accelerationIncludingGravity.x||ev.accelerationIncludingGravity.y||ev.accelerationIncludingGravity.z; - if(window.orientation){ - var or = window.orientation; - if (typeof or === "number") { - this.addEntropy(or, 1, "accelerometer"); - } - } - if (ac) { - this.addEntropy(ac, 2, "accelerometer"); - } - this._addCurrentTimeToEntropy(0); - }, - - _fireEvent: function (name, arg) { - var j, cbs=sjcl.random._callbacks[name], cbsTemp=[]; - /* TODO: there is a race condition between removing collectors and firing them */ - - /* I'm not sure if this is necessary; in C++, iterating over a - * collection and modifying it at the same time is a no-no. - */ - - for (j in cbs) { - if (cbs.hasOwnProperty(j)) { - cbsTemp.push(cbs[j]); - } - } - - for (j=0; j 4)) { - throw new sjcl.exception.invalid("json encrypt: invalid parameters"); - } - - if (typeof password === "string") { - tmp = sjcl.misc.cachedPbkdf2(password, p); - password = tmp.key.slice(0,p.ks/32); - p.salt = tmp.salt; - } else if (sjcl.ecc && password instanceof sjcl.ecc.elGamal.publicKey) { - tmp = password.kem(); - p.kemtag = tmp.tag; - password = tmp.key.slice(0,p.ks/32); - } - if (typeof plaintext === "string") { - plaintext = sjcl.codec.utf8String.toBits(plaintext); - } - if (typeof adata === "string") { - p.adata = adata = sjcl.codec.utf8String.toBits(adata); - } - prp = new sjcl.cipher[p.cipher](password); - - /* return the json data */ - j._add(rp, p); - rp.key = password; - - /* do the encryption */ - if (p.mode === "ccm" && sjcl.arrayBuffer && sjcl.arrayBuffer.ccm && plaintext instanceof ArrayBuffer) { - p.ct = sjcl.arrayBuffer.ccm.encrypt(prp, plaintext, p.iv, adata, p.ts); - } else { - p.ct = sjcl.mode[p.mode].encrypt(prp, plaintext, p.iv, adata, p.ts); - } - - //return j.encode(j._subtract(p, j.defaults)); - return p; - }, - - /** Simple encryption function. - * @param {String|bitArray} password The password or key. - * @param {String} plaintext The data to encrypt. - * @param {Object} [params] The parameters including tag, iv and salt. - * @param {Object} [rp] A returned version with filled-in parameters. - * @return {String} The ciphertext serialized data. - * @throws {sjcl.exception.invalid} if a parameter is invalid. - */ - encrypt: function (password, plaintext, params, rp) { - var j = sjcl.json, p = j._encrypt.apply(j, arguments); - return j.encode(p); - }, - - /** Simple decryption function. - * @param {String|bitArray} password The password or key. - * @param {Object} ciphertext The cipher raw data to decrypt. - * @param {Object} [params] Additional non-default parameters. - * @param {Object} [rp] A returned object with filled parameters. - * @return {String} The plaintext. - * @throws {sjcl.exception.invalid} if a parameter is invalid. - * @throws {sjcl.exception.corrupt} if the ciphertext is corrupt. - */ - _decrypt: function (password, ciphertext, params, rp) { - params = params || {}; - rp = rp || {}; - - var j = sjcl.json, p = j._add(j._add(j._add({},j.defaults),ciphertext), params, true), ct, tmp, prp, adata=p.adata; - if (typeof p.salt === "string") { - p.salt = sjcl.codec.base64.toBits(p.salt); - } - if (typeof p.iv === "string") { - p.iv = sjcl.codec.base64.toBits(p.iv); - } - - if (!sjcl.mode[p.mode] || - !sjcl.cipher[p.cipher] || - (typeof password === "string" && p.iter <= 100) || - (p.ts !== 64 && p.ts !== 96 && p.ts !== 128) || - (p.ks !== 128 && p.ks !== 192 && p.ks !== 256) || - (!p.iv) || - (p.iv.length < 2 || p.iv.length > 4)) { - throw new sjcl.exception.invalid("json decrypt: invalid parameters"); - } - - if (typeof password === "string") { - tmp = sjcl.misc.cachedPbkdf2(password, p); - password = tmp.key.slice(0,p.ks/32); - p.salt = tmp.salt; - } else if (sjcl.ecc && password instanceof sjcl.ecc.elGamal.secretKey) { - password = password.unkem(sjcl.codec.base64.toBits(p.kemtag)).slice(0,p.ks/32); - } - if (typeof adata === "string") { - adata = sjcl.codec.utf8String.toBits(adata); - } - prp = new sjcl.cipher[p.cipher](password); - - /* do the decryption */ - if (p.mode === "ccm" && sjcl.arrayBuffer && sjcl.arrayBuffer.ccm && p.ct instanceof ArrayBuffer) { - ct = sjcl.arrayBuffer.ccm.decrypt(prp, p.ct, p.iv, p.tag, adata, p.ts); - } else { - ct = sjcl.mode[p.mode].decrypt(prp, p.ct, p.iv, adata, p.ts); - } - - /* return the json data */ - j._add(rp, p); - rp.key = password; - - if (params.raw === 1) { - return ct; - } else { - return sjcl.codec.utf8String.fromBits(ct); - } - }, - - /** Simple decryption function. - * @param {String|bitArray} password The password or key. - * @param {String} ciphertext The ciphertext to decrypt. - * @param {Object} [params] Additional non-default parameters. - * @param {Object} [rp] A returned object with filled parameters. - * @return {String} The plaintext. - * @throws {sjcl.exception.invalid} if a parameter is invalid. - * @throws {sjcl.exception.corrupt} if the ciphertext is corrupt. - */ - decrypt: function (password, ciphertext, params, rp) { - var j = sjcl.json; - return j._decrypt(password, j.decode(ciphertext), params, rp); - }, - - /** Encode a flat structure into a JSON string. - * @param {Object} obj The structure to encode. - * @return {String} A JSON string. - * @throws {sjcl.exception.invalid} if obj has a non-alphanumeric property. - * @throws {sjcl.exception.bug} if a parameter has an unsupported type. - */ - encode: function (obj) { - var i, out='{', comma=''; - for (i in obj) { - if (obj.hasOwnProperty(i)) { - if (!i.match(/^[a-z0-9]+$/i)) { - throw new sjcl.exception.invalid("json encode: invalid property name"); - } - out += comma + '"' + i + '":'; - comma = ','; - - switch (typeof obj[i]) { - case 'number': - case 'boolean': - out += obj[i]; - break; - - case 'string': - out += '"' + escape(obj[i]) + '"'; - break; - - case 'object': - out += '"' + sjcl.codec.base64.fromBits(obj[i],0) + '"'; - break; - - default: - throw new sjcl.exception.bug("json encode: unsupported type"); - } - } - } - return out+'}'; - }, - - /** Decode a simple (flat) JSON string into a structure. The ciphertext, - * adata, salt and iv will be base64-decoded. - * @param {String} str The string. - * @return {Object} The decoded structure. - * @throws {sjcl.exception.invalid} if str isn't (simple) JSON. - */ - decode: function (str) { - str = str.replace(/\s/g,''); - if (!str.match(/^\{.*\}$/)) { - throw new sjcl.exception.invalid("json decode: this isn't json!"); - } - var a = str.replace(/^\{|\}$/g, '').split(/,/), out={}, i, m; - for (i=0; i bn) return 1 + return 0 + }//}}} + static create(name, parentUUID) {// {{{ + const node = new Node({ + UUID: uuidv7(), + Created: (new Date()).toISOString(), + Content: '', + Name: name, + ParentUUID: parentUUID, + Markdown: false, + }) + + // Newly created node (not constructed from existing data) is considered modified + // since node.save returns early if it isn't modified. + node._modified = true + + return node + }// }}} + + constructor(nodeData, level) {//{{{ + this.Level = level + this.data = nodeData + this.UUID = nodeData.UUID + + // Toplevel nodes are normalized to have the ROOT_NODE as parent. + if (nodeData.UUID !== ROOT_NODE && nodeData.ParentUUID === '') { + this.ParentUUID = ROOT_NODE + this.data.ParentUUID = ROOT_NODE + } else + this.ParentUUID = nodeData.ParentUUID + + this._children_fetched = false + this._has_children = null // this will be set by nodeStore.getTreeNodes + this.Children = [] + this.Ancestors = [] + + this._sibling_before = null + this._sibling_after = null + this._parent = null + + this.reset() + }//}}} + + reset() {// {{{ + // this._content is the runtime content in the editor. + // this.data.Content is the original data. + this._content = this.data.Content + this._modified = false + }// }}} + get(prop) {//{{{ + return this.data[prop] + }//}}} + updated() {//{{{ + // '2024-12-17T17:33:48.85939Z + return new Date(Date.parse(this.data.Updated)) + }//}}} + isModified() {// {{{ + return this._modified + }// }}} + hasFetchedChildren() {//{{{ + return this._children_fetched + }//}}} + async fetchChildren() {//{{{ + this.Children = await globalThis.nodeStore.getTreeNodes(this.UUID, this.Level + 1) + this._children_fetched = true + + // Children are sorted to allow for storing siblings befare and after. + // These are used with keyboard navigation in the tree. + this.Children.sort(Node.sort) + + const numChildren = this.Children.length + this.setHasChildren(numChildren > 0) + for (let i = 0; i < numChildren; i++) { + if (i > 0) + this.Children[i]._sibling_before = this.Children[i - 1] + if (i < numChildren - 1) + this.Children[i]._sibling_after = this.Children[i + 1] + this.Children[i]._parent = this + } + + return this.Children + }//}}} + setHasChildren(v) {// {{{ + this._has_children = v + }// }}} + hasChildren() {//{{{ + return this._has_children + }//}}} + getSiblingBefore() {// {{{ + return this._sibling_before + }// }}} + getSiblingAfter() {// {{{ + return this._sibling_after + }// }}} + getParent() {//{{{ + return this._parent + }//}}} + moveToParent(newParentUUID) {// {{{ + if (this.UUID === newParentUUID) + throw new Error("New parent UUID is the same as node UUID. Can't be your own parent.") + + this.ParentUUID = newParentUUID + this.data.ParentUUID = newParentUUID + this._modified = true + }// }}} + isLastSibling() {//{{{ + return this._sibling_after === null + }//}}} + isFirstSibling() {//{{{ + return this._sibling_before === null + }//}}} + isSpecial() {// {{{ + return this.data.Special + }// }}} + content() {//{{{ + // TODO - implement crypto + return this._content + }//}}} + setContent(new_content) {//{{{ + this._content = new_content + this._modified = true + _mbus.dispatch('NODE_MODIFIED', { node: this }) + }//}}} + setName(new_name) {// {{{ + if (new_name.trim() === '') + throw new Error(`The name can't be empty`) + + this.data.Name = new_name + this._modified = true + _mbus.dispatch('NODE_MODIFIED', { node: this }) + }// }}} + async save() {//{{{ + try { + const dblink = /db:\/\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/gi + + // Just safeguarding not using the root node, + // which sort of exist but isn't supposed to communicate to server. + if (this.UUID == ROOT_NODE) + return + + // File links from the original, unmodified content are found to compare to links from the new content. + // All files (images) needs to be indexed to nodes in order to track orphaned files. + const before = new Set([...this.data.Content.matchAll(dblink)].map(m => m[1])) + const after = new Set([...this._content.matchAll(dblink)].map(m => m[1])) + const removed = before.difference(after) + const added = after.difference(before) + + await globalThis.nodeStore.filesNodelink.removeFileUUIDs(this.UUID, removed) + await globalThis.nodeStore.filesNodelink.addFileUUIDs(this.UUID, added) + + // Authoritative content and metadata is set to prepare it for IndexedDB update. + this.data.Content = this._content + this.data.Updated = new Date().toISOString() + this.data.HistoryUUID = uuidv7() // every time the node is saved a new history UUID identifies the changed node. + + _mbus.dispatch('NODE_UNMODIFIED') + this._modified = false + + // When stored into database and ancestry was changed, + // the ancestry path could be interesting. + /* + const ancestors = await nodeStore.getNodeAncestry(this) + this.data.Ancestors = ancestors.map(a => a.get('Name')).reverse() + */ + + /* The node history is a local store for node history. + * This could be provisioned from the server or cleared if + * deemed unnecessary. + * + * The send queue is what will be sent back to the server + * to have a recorded history of the notes. + * + * A setting to be implemented in the future could be to + * not save the history locally at all. */ + + // Current node is added to history. It will be duplicated with the "nodes" store + // for simplicity, to hopefully avoid bugs. + const history = globalThis.nodeStore.nodesHistory.add(this) + + // Updated node is added to the send queue to be stored on server. + + const sendQueue = globalThis.nodeStore.sendQueue.add(this) + + // Updated node is saved to the primary node store. + const nodeStoreAdding = globalThis.nodeStore.add([this]) + + await Promise.all([history, sendQueue, nodeStoreAdding]) + + } catch (err) { + console.error(err) + alert(err.message) + } + + return + }//}}} +} + export class NodeStore { constructor() {//{{{ - if (!('indexedDB' in window)) { + if (!('indexedDB' in self)) { throw 'Missing IndexedDB' } @@ -21,7 +227,7 @@ export class NodeStore { }//}}} initializeDB() {//{{{ return new Promise((resolve, reject) => { - const req = indexedDB.open('notes', 12) + const req = indexedDB.open('notes', 14) // Schema upgrades for IndexedDB. // These can start from different points depending on updates to Notes2 since a device was online. @@ -31,7 +237,6 @@ export class NodeStore { let sendQueue let nodesHistory let files - let filesNodelink const db = event.target.result const trx = event.target.transaction @@ -89,6 +294,14 @@ export class NodeStore { case 12: trx.objectStore('files_nodelink').createIndex('byNodeCount', 'NodeCount', { unique: false }) break + + case 13: + db.createObjectStore('file_segments', { keyPath: ['UUID', 'Sequence'] }) + break + + case 14: + db.deleteObjectStore('files') + break } } } @@ -98,6 +311,7 @@ export class NodeStore { this.sendQueue = new SimpleNodeStore(this.db, 'send_queue') this.nodesHistory = new NodeHistoryStore(this.db, 'nodes_history') this.files = new SimpleNodeStore(this.db, 'files') + this.fileSegments = new FileSegmentsStore(this.db, 'file_segments') this.filesMetadata = new FilesMetadataStore(this.db, 'files_metadata') this.filesNodelink = new FileNodesLinkStore(this.db, 'files_nodelink') resolve() @@ -353,9 +567,38 @@ export class NodeStore { }//}}} async storeFile(file) {// {{{ - const metadata = new FileMetadata(file, {}) + const metadata = new FileMetadata(file) + + try { + // Do potentially larger blocks. + const BLOCKSIZE = 1048576 + const stream = file.stream() + const reader = stream.getReader({ mode: 'byob' }) + let buffer = new Uint8Array(BLOCKSIZE) + + let seq = 0 + while (true) { + const { done, value } = await reader.read(buffer) + if (done) break + + // Block is added as a file segment. + await this.fileSegments.add({ + data: { + UUID: metadata.UUID, + Sequence: seq, + Data: value, + } + }) + buffer = new Uint8Array(value.buffer) + seq++ + } + + reader.releaseLock() + } catch (e) { + console.error(e) + alert(e.message) + } - await this.files.add({ data: { UUID: metadata.UUID, file } }) await this.filesMetadata.add({ data: metadata }) return metadata @@ -374,30 +617,11 @@ export class NodeStore { // No more files to sync. if (data === undefined) { - resolve({ file: null, metadata: null }) + resolve(null) return } - // Retrieve file as well. - const filereq = this.db. - transaction('files', 'readonly') - .objectStore('files') - .get(data.UUID) - - filereq.onsuccess = (e) => { - const f = e.target.result - - // File should always be in IndexedDB when metadata is. - if (f === undefined) { - reject(new Error(`File with UUID '${data.UUID}' is missing.`)) - return - } - - resolve({ - file: f.file, - metadata: new FileMetadata(f.file, data), - }) - } + resolve(new FileMetadata(data)) } }) }// }}} @@ -528,7 +752,7 @@ class NodeHistoryStore extends SimpleNodeStore { } req.onerror = (event) => { - console.log(event.target.error) + console.error(event.target.error) reject(event.target.error) } }) @@ -581,6 +805,37 @@ class NodeHistoryStore extends SimpleNodeStore { }// }}} } +class FileSegmentsStore extends SimpleNodeStore { + constructor(db, storeName) {//{{{ + super(db, storeName) + }//}}} + async getStream(uuid) {// {{{ + let seq = 0 + const meself = this + + return new ReadableStream({ + type: 'bytes', + + async pull(controller) { + try { + while (true) { + const chunk = await meself.get([uuid, seq]) + if (!chunk) { + controller.close() + return + } + + controller.enqueue(chunk.Data) + seq++ + } + } catch (e) { + console.error(e) + } + } + }) + }// }}} +} + class FilesMetadataStore extends SimpleNodeStore { countToUpload() {// {{{ const store = this.db @@ -638,6 +893,32 @@ class FileNodesLinkStore extends SimpleNodeStore { }// }}} } +class FileMetadata { + constructor(data) {// {{{ + console.log('md', data) + this.UUID = data.UUID || uuidv7() + this.uploaded = data.uploaded || 'only_on_device' + + this.name = data.name + this.size = data.size + this.type = data.type || 'application/octet-stream' + this.modified = data.modified || data.lastModified || Date.now() + }// }}} + setUploaded() {// {{{ + this.uploaded = 'on_server' + }// }}} + get data() {// {{{ + return { + UUID: this.UUID, + uploaded: this.uploaded, + name: this.name, + size: this.size, + type: this.type, + modified: this.modified, + } + }// }}} +} + export function uuidv7() {// {{{ // random bytes const value = new Uint8Array(16) @@ -664,29 +945,4 @@ export function uuidv7() {// {{{ return `${str.slice(0, 8)}-${str.slice(8, 12)}-${str.slice(12, 16)}-${str.slice(16, 20)}-${str.slice(20)}` }// }}} -class FileMetadata { - constructor(file, data) {// {{{ - this.UUID = data.UUID || uuidv7() - this.uploaded = data.uploaded || 'only_on_device' - - this.name = file.name - this.size = file.size - this.type = file.type || 'application/octet-stream' - this.modified = file.lastModified - }// }}} - setUploaded() {// {{{ - this.uploaded = 'on_server' - }// }}} - get data() {// {{{ - return { - UUID: this.UUID, - uploaded: this.uploaded, - name: this.name, - size: this.size, - type: this.type, - modified: this.modified, - } - }// }}} -} - // vim: foldmethod=marker diff --git a/static/js/page_history.mjs b/static/js/page_history.mjs index 44061f6..2c03193 100644 --- a/static/js/page_history.mjs +++ b/static/js/page_history.mjs @@ -1,5 +1,5 @@ import { CustomHTMLElement } from './lib/custom_html_element.mjs' -import { Node } from './page_node.mjs' +import { Node } from 'node_store' import { MarkedPosition } from './marked_position.mjs' diff --git a/static/js/page_node.mjs b/static/js/page_node.mjs index 1568c92..574fe96 100644 --- a/static/js/page_node.mjs +++ b/static/js/page_node.mjs @@ -1,4 +1,3 @@ -import { ROOT_NODE, uuidv7 } from 'node_store' import { CustomHTMLElement } from './lib/custom_html_element.mjs' import { MarkedPosition } from './marked_position.mjs' @@ -398,213 +397,4 @@ export class N2PageNodeUI extends CustomHTMLElement { } customElements.define('n2-nodeui', N2PageNodeUI) -export class Node { - static sort(a, b) {//{{{ - // Nodes with children ("folders") are sorted first. - if (a._has_children && !b._has_children) return -1 - if (!a._has_children && b._has_children) return 1 - - // Otherwise sort by lowercased name. - const an = a.data.Name.toLowerCase() - const bn = b.data.Name.toLowerCase() - if (an < bn) return -1 - if (an > bn) return 1 - return 0 - }//}}} - static create(name, parentUUID) {// {{{ - const node = new Node({ - UUID: uuidv7(), - Created: (new Date()).toISOString(), - Content: '', - Name: name, - ParentUUID: parentUUID, - Markdown: false, - }) - - // Newly created node (not constructed from existing data) is considered modified - // since node.save returns early if it isn't modified. - node._modified = true - - return node - }// }}} - - constructor(nodeData, level) {//{{{ - this.Level = level - this.data = nodeData - this.UUID = nodeData.UUID - - // Toplevel nodes are normalized to have the ROOT_NODE as parent. - if (nodeData.UUID !== ROOT_NODE && nodeData.ParentUUID === '') { - this.ParentUUID = ROOT_NODE - this.data.ParentUUID = ROOT_NODE - } else - this.ParentUUID = nodeData.ParentUUID - - this._children_fetched = false - this._has_children = null // this will be set by nodeStore.getTreeNodes - this.Children = [] - this.Ancestors = [] - - this._sibling_before = null - this._sibling_after = null - this._parent = null - - this.reset() - }//}}} - - reset() {// {{{ - // this._content is the runtime content in the editor. - // this.data.Content is the original data. - this._content = this.data.Content - this._modified = false - }// }}} - get(prop) {//{{{ - return this.data[prop] - }//}}} - updated() {//{{{ - // '2024-12-17T17:33:48.85939Z - return new Date(Date.parse(this.data.Updated)) - }//}}} - isModified() {// {{{ - return this._modified - }// }}} - hasFetchedChildren() {//{{{ - return this._children_fetched - }//}}} - async fetchChildren() {//{{{ - this.Children = await nodeStore.getTreeNodes(this.UUID, this.Level + 1) - this._children_fetched = true - - // Children are sorted to allow for storing siblings befare and after. - // These are used with keyboard navigation in the tree. - this.Children.sort(Node.sort) - - const numChildren = this.Children.length - this.setHasChildren(numChildren > 0) - for (let i = 0; i < numChildren; i++) { - if (i > 0) - this.Children[i]._sibling_before = this.Children[i - 1] - if (i < numChildren - 1) - this.Children[i]._sibling_after = this.Children[i + 1] - this.Children[i]._parent = this - } - - return this.Children - }//}}} - setHasChildren(v) {// {{{ - this._has_children = v - }// }}} - hasChildren() {//{{{ - return this._has_children - }//}}} - getSiblingBefore() {// {{{ - return this._sibling_before - }// }}} - getSiblingAfter() {// {{{ - return this._sibling_after - }// }}} - getParent() {//{{{ - return this._parent - }//}}} - moveToParent(newParentUUID) {// {{{ - if (this.UUID === newParentUUID) - throw new Error("New parent UUID is the same as node UUID. Can't be your own parent.") - - this.ParentUUID = newParentUUID - this.data.ParentUUID = newParentUUID - this._modified = true - }// }}} - isLastSibling() {//{{{ - return this._sibling_after === null - }//}}} - isFirstSibling() {//{{{ - return this._sibling_before === null - }//}}} - isSpecial() {// {{{ - return this.data.Special - }// }}} - content() {//{{{ - // TODO - implement crypto - return this._content - }//}}} - setContent(new_content) {//{{{ - this._content = new_content - this._modified = true - _mbus.dispatch('NODE_MODIFIED', { node: this }) - }//}}} - setName(new_name) {// {{{ - if (new_name.trim() === '') - throw new Error(`The name can't be empty`) - - this.data.Name = new_name - this._modified = true - _mbus.dispatch('NODE_MODIFIED', { node: this }) - }// }}} - async save() {//{{{ - try { - const dblink = /db:\/\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/gi - - // Just safeguarding not using the root node, - // which sort of exist but isn't supposed to communicate to server. - if (this.UUID == ROOT_NODE) - return - - // File links from the original, unmodified content are found to compare to links from the new content. - // All files (images) needs to be indexed to nodes in order to track orphaned files. - const before = new Set([...this.data.Content.matchAll(dblink)].map(m => m[1])) - const after = new Set([...this._content.matchAll(dblink)].map(m => m[1])) - const removed = before.difference(after) - const added = after.difference(before) - - await nodeStore.filesNodelink.removeFileUUIDs(this.UUID, removed) - await nodeStore.filesNodelink.addFileUUIDs(this.UUID, added) - - // Authoritative content and metadata is set to prepare it for IndexedDB update. - this.data.Content = this._content - this.data.Updated = new Date().toISOString() - this.data.HistoryUUID = uuidv7() // every time the node is saved a new history UUID identifies the changed node. - - _mbus.dispatch('NODE_UNMODIFIED') - this._modified = false - - // When stored into database and ancestry was changed, - // the ancestry path could be interesting. - /* - const ancestors = await nodeStore.getNodeAncestry(this) - this.data.Ancestors = ancestors.map(a => a.get('Name')).reverse() - */ - - /* The node history is a local store for node history. - * This could be provisioned from the server or cleared if - * deemed unnecessary. - * - * The send queue is what will be sent back to the server - * to have a recorded history of the notes. - * - * A setting to be implemented in the future could be to - * not save the history locally at all. */ - - // Current node is added to history. It will be duplicated with the "nodes" store - // for simplicity, to hopefully avoid bugs. - const history = nodeStore.nodesHistory.add(this) - - // Updated node is added to the send queue to be stored on server. - - const sendQueue = nodeStore.sendQueue.add(this) - - // Updated node is saved to the primary node store. - const nodeStoreAdding = nodeStore.add([this]) - - await Promise.all([history, sendQueue, nodeStoreAdding]) - - } catch (err) { - console.error(err) - alert(err.message) - } - - return - }//}}} -} - - // vim: foldmethod=marker diff --git a/static/js/sidebar.mjs b/static/js/sidebar.mjs index b2204a0..899e955 100644 --- a/static/js/sidebar.mjs +++ b/static/js/sidebar.mjs @@ -1,5 +1,4 @@ -import { ROOT_NODE, ORPHANED_NODE, DELETED_NODE } from 'node_store' -import { Node } from 'node' +import { ROOT_NODE, ORPHANED_NODE, DELETED_NODE, Node } from 'node_store' import { CustomHTMLElement } from './lib/custom_html_element.mjs' import { Color, Solver } from './lib/css_colorize.mjs' diff --git a/static/js/sync.mjs b/static/js/sync.mjs index 7991644..f68a09f 100644 --- a/static/js/sync.mjs +++ b/static/js/sync.mjs @@ -1,5 +1,5 @@ import { API } from 'api' -import { Node } from 'node' +import { Node } from 'node_store' import { CustomHTMLElement } from './lib/custom_html_element.mjs' export class Sync { @@ -169,18 +169,18 @@ export class Sync { const BLOCKSIZE = 128 * 1024 let sentBytes = 0 - const { file, metadata } = await nodeStore.nextFileToSync() - if (file === null) + const metadata = await nodeStore.nextFileToSync() + if (metadata === null) return - const stream = file.stream() + const stream = await globalThis.nodeStore.fileSegments.getStream(metadata.UUID) const reader = stream.getReader({ mode: 'byob' }) // Bring Your Own Buffer let buffer = new Uint8Array(BLOCKSIZE) while (true) { const { value, done } = await reader.read(buffer) - await API.upload(metadata.UUID, value, sentBytes, file) + await API.upload(metadata.UUID, value, sentBytes, metadata) metadata.setUploaded() nodeStore.filesMetadata.add(metadata) @@ -189,6 +189,23 @@ export class Sync { sentBytes += value.length buffer = new Uint8Array(value.buffer) } + reader.releaseLock() + } catch (e) { + console.error(e) + alert(e.message) + } + }// }}} + + async filesFromServer() {// {{{ + let offset = 0 + let more = true + + try { + while (more) { + const res = await API.query('GET', `/file/server_uuids/${offset}`) + more = res.MoreRowsExist + console.log(res) + } } catch (e) { console.error(e) alert(e.message) diff --git a/static/service_worker.js b/static/service_worker.js index 0f71246..7cf268e 100644 --- a/static/service_worker.js +++ b/static/service_worker.js @@ -1,3 +1,5 @@ +import { NodeStore } from '/js/{{ .VERSION }}/node_store.mjs' + const CACHE_NAME = 'notes2-{{ .VERSION }}' const CACHED_ASSETS = [ '/', @@ -33,14 +35,11 @@ const CACHED_ASSETS = [ '/js/{{ .VERSION }}/api.mjs', '/js/{{ .VERSION }}/app.mjs', '/js/{{ .VERSION }}/checklist.mjs', - '/js/{{ .VERSION }}/crypto.mjs', '/js/{{ .VERSION }}/file.mjs', - '/js/{{ .VERSION }}/key.mjs', '/js/{{ .VERSION }}/lib/css_colorize.mjs', '/js/{{ .VERSION }}/lib/custom_html_element.mjs', '/js/{{ .VERSION }}/lib/node_modules/marked/lib/marked.esm.js', '/js/{{ .VERSION }}/lib/node_modules/marked-token-position/lib/index.esm.js', - '/js/{{ .VERSION }}/lib/sjcl.js', '/js/{{ .VERSION }}/marked_position.mjs', '/js/{{ .VERSION }}/mbus.mjs', '/js/{{ .VERSION }}/node_store.mjs', @@ -126,14 +125,55 @@ self.addEventListener('activate', event => { }) self.addEventListener('fetch', event => { + const url = new URL(event.request.url) + // The fetch event is also seeing requests to other domains. // Just let the browser handle those for itself. - const ourDomain = event.request.url.startsWith(self.location.origin) + const ourDomain = (url.host == self.location.host) if (!ourDomain) return event + if (url.pathname == '/stream-local') { + event.respondWith(indexeddbFile(event)) + return + } + if (`{{ .DevMode }}` == 'true') return event - - event.respondWith(fetchAsset(event)) + else + event.respondWith(fetchAsset(event)) }) + + +let nodestore = null +async function getNodestore() { + if (nodestore) + return nodestore + + // When setting nodestore directly, then waiting on initializeDB, + // a second call for getNodestore can return the nodestore before + // initialized. + const initializingNodeStore = new NodeStore() + await initializingNodeStore.initializeDB() + nodestore = initializingNodeStore + return nodestore +} + +async function indexeddbFile(event) { + const url = new URL(event.request.url) + const uuid = url.searchParams.get('uuid') + + try { + const nodeStore = await getNodestore() + const md = await nodeStore.filesMetadata.get(uuid) + const stream = await nodeStore.fileSegments.getStream(uuid) + + return new Response(stream, { + headers: { + 'Content-Type': md.type, + } + }) + } catch (err) { + console.error(err) + } +} diff --git a/views/layouts/main.gotmpl b/views/layouts/main.gotmpl index c5fbead..13a0bcf 100644 --- a/views/layouts/main.gotmpl +++ b/views/layouts/main.gotmpl @@ -9,10 +9,8 @@ { "imports": { "api": "/js/{{ .VERSION }}/api.mjs", - "sync": "/js/{{ .VERSION }}/sync.mjs", - "key": "/js/{{ .VERSION }}/key.mjs", + "sync": "/js/{{ .VERSION }}/sync.mjs", "checklist": "/js/{{ .VERSION }}/checklist.mjs", - "crypto": "/js/{{ .VERSION }}/crypto.mjs", "node_store": "/js/{{ .VERSION }}/node_store.mjs", "node": "/js/{{ .VERSION }}/page_node.mjs", "sidebar": "/js/{{ .VERSION }}/sidebar.mjs" @@ -20,14 +18,14 @@ } diff --git a/views/pages/login.gotmpl b/views/pages/login.gotmpl index 3e2235f..8e824aa 100644 --- a/views/pages/login.gotmpl +++ b/views/pages/login.gotmpl @@ -4,7 +4,7 @@ - +
@@ -42,6 +42,6 @@ class Login { } } -window._login = new Login() +globalThis._login = new Login() {{ end }} diff --git a/views/pages/notes2.gotmpl b/views/pages/notes2.gotmpl index 2755aea..c5df63e 100644 --- a/views/pages/notes2.gotmpl +++ b/views/pages/notes2.gotmpl @@ -7,7 +7,7 @@
-
>
+
>
@@ -47,20 +47,21 @@ import {App} from "/js/{{ .VERSION }}/app.mjs" import {API} from 'api' import {Sync} from 'sync' + import { } from '/js/{{ .VERSION }}/page_node.mjs' import { } from '/js/{{ .VERSION }}/page_preferences.mjs' import { } from '/js/{{ .VERSION }}/page_storage.mjs' import { } from '/js/{{ .VERSION }}/page_history.mjs' import { } from '/js/{{ .VERSION }}/file.mjs' - window.Sync = Sync + globalThis.Sync = Sync if (!API.hasAuthenticationToken()) { location.href = '/login' } else { try { - window.nodeStore = new NodeStore() - window.nodeStore.initializeDB().then(() => { - window._app = new App() + globalThis.nodeStore = new NodeStore() + globalThis.nodeStore.initializeDB().then(() => { + globalThis._app = new App() }) } catch (e) { alert(e) diff --git a/views/pages/sync.gotmpl b/views/pages/sync.gotmpl index eb79c77..c9fa870 100644 --- a/views/pages/sync.gotmpl +++ b/views/pages/sync.gotmpl @@ -5,9 +5,9 @@