diff --git a/key.go b/key.go new file mode 100644 index 0000000..fc3fe14 --- /dev/null +++ b/key.go @@ -0,0 +1,34 @@ +package main + +import ( + // External + "github.com/jmoiron/sqlx" + + // Standard +) + +type Key struct { + ID int + UserID int `db:"user_id"` + Description string + Key string +} + +func (session Session) Keys() (keys []Key, err error) { + var rows *sqlx.Rows + if rows, err = db.Queryx(`SELECT * FROM crypto_key WHERE user_id=$1`, session.UserID); err != nil { + return + } + defer rows.Close() + + keys = []Key{} + for rows.Next() { + key := Key{} + if err = rows.StructScan(&key); err != nil { + return + } + keys = append(keys, key) + } + + return +} diff --git a/main.go b/main.go index 291b7d5..1bc2819 100644 --- a/main.go +++ b/main.go @@ -22,7 +22,7 @@ import ( const VERSION = "v0.2.2"; const LISTEN_HOST = "0.0.0.0"; -const DB_SCHEMA = 6 +const DB_SCHEMA = 7 var ( flagPort int @@ -76,6 +76,7 @@ func main() {// {{{ http.HandleFunc("/node/delete", nodeDelete) http.HandleFunc("/node/upload", nodeUpload) http.HandleFunc("/node/download", nodeDownload) + http.HandleFunc("/key/retrieve", keyRetrieve) http.HandleFunc("/ws", websocketHandler) http.HandleFunc("/", staticHandler) @@ -540,6 +541,28 @@ func nodeFiles(w http.ResponseWriter, r *http.Request) {// {{{ }) }// }}} +func keyRetrieve(w http.ResponseWriter, r *http.Request) {// {{{ + log.Println("/key/retrieve") + var err error + var session Session + + if session, _, err = ValidateSession(r, true); err != nil { + responseError(w, err) + return + } + + keys, err := session.Keys() + if err != nil { + responseError(w, err) + return + } + + responseData(w, map[string]interface{}{ + "OK": true, + "Keys": keys, + }) +}// }}} + func newTemplate(requestPath string) (tmpl *template.Template, err error) {// {{{ // Append index.html if needed for further reading of the file p := requestPath diff --git a/sql/0007.sql b/sql/0007.sql new file mode 100644 index 0000000..73ac4c6 --- /dev/null +++ b/sql/0007.sql @@ -0,0 +1,10 @@ +CREATE TABLE public.crypto_key ( + id serial NOT NULL, + user_id int4 NOT NULL, + description varchar(255) NOT NULL DEFAULT '', + "key" char(144) NOT NULL, + CONSTRAINT crypto_key_pk PRIMARY KEY (id), + CONSTRAINT crypto_key_fk FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE CASCADE ON UPDATE CASCADE +); + +COMMENT ON COLUMN public.crypto_key.key IS 'salt(16 bytes) + [key encrypted with pbkdf2(pass, salt)]'; diff --git a/static/css/main.css b/static/css/main.css index 0d2ab2d..0c7c6f2 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -19,16 +19,8 @@ body { height: 100%; } h1 { - color: #abc837; -} -.layout-crumbs { - grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "files" "blank"; - grid-template-columns: 1fr; - grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* files */ 1fr; - /* blank */ -} -.layout-crumbs #tree { - display: none; + margin-top: 0px; + font-size: 1em; } #blackout { position: absolute; @@ -113,10 +105,21 @@ h1 { #upload .files .progress.done { color: #0a0; } +#properties { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: #333; + background: #fff; + border: 2px solid #000; + padding: 16px; + z-index: 1025; +} header { display: grid; grid-area: header; - grid-template-columns: min-content 1fr min-content min-content; + grid-template-columns: min-content 1fr repeat(3, min-content); align-items: center; padding: 0px; color: #333c11; @@ -149,6 +152,13 @@ header .add { padding-right: 16px; cursor: pointer; } +header .keys { + padding-right: 16px; +} +header .keys img { + cursor: pointer; + height: 24px; +} header .menu { font-size: 1.25em; padding-right: 16px; @@ -341,6 +351,34 @@ header .menu { white-space: nowrap; text-align: right; } +#keys { + padding: 32px; + color: #333; +} +#keys .key-list { + display: grid; + grid-template-columns: min-content 1fr; + grid-gap: 12px 12px; + align-items: end; + margin-top: 16px; + font-size: 0.85em; +} +#keys .key-list .status { + cursor: pointer; +} +#keys .key-list .status img { + height: 32px; +} +#keys .key-list .status .locked { + color: #a00; +} +#keys .key-list .status .unlocked { + color: #0a0; +} +#keys .key-list .description { + cursor: pointer; + padding-bottom: 4px; +} .layout-tree { display: grid; grid-template-areas: "header header" "tree crumbs" "tree child-nodes" "tree name" "tree content" "tree files" "tree blank"; @@ -386,6 +424,36 @@ header .menu { .layout-crumbs #tree { display: none; } +.layout-keys { + display: grid; + grid-template-areas: "header" "keys"; + grid-template-columns: 1fr; + grid-template-rows: min-content /* header */ 1fr; + /* blank */ + color: #fff; + height: 100%; +} +.layout-keys #crumbs { + display: none; +} +.layout-keys .child-nodes { + display: none; +} +.layout-keys .node-name { + display: none; +} +.layout-keys .grow-wrap { + display: none; +} +.layout-keys #file-section { + display: none; +} +.layout-keys #tree { + display: none; +} +.layout-keys #keys { + display: block; +} #app { display: grid; grid-template-areas: "header header" "tree crumbs" "tree child-nodes" "tree name" "tree content" "tree files" "tree blank"; @@ -396,7 +464,6 @@ header .menu { height: 100%; } #app.toggle-tree { - /* blank */ grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "files" "blank"; grid-template-columns: 1fr; grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* files */ 1fr; @@ -405,12 +472,8 @@ header .menu { #app.toggle-tree #tree { display: none; } -#app.toggle-tree #tree { - display: none; -} @media only screen and (max-width: 932px) { #app { - /* blank */ grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "files" "blank"; grid-template-columns: 1fr; grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* files */ 1fr; @@ -419,9 +482,6 @@ header .menu { #app #tree { display: none; } - #app #tree { - display: none; - } #app.toggle-tree { display: grid; grid-template-areas: "header" "tree"; diff --git a/static/images/padlock-closed.svg b/static/images/padlock-closed.svg new file mode 100644 index 0000000..f33e37c --- /dev/null +++ b/static/images/padlock-closed.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/static/images/padlock-open.svg b/static/images/padlock-open.svg new file mode 100644 index 0000000..0c6c0f3 --- /dev/null +++ b/static/images/padlock-open.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/static/images/padlock.svg b/static/images/padlock.svg new file mode 100644 index 0000000..320b7cc --- /dev/null +++ b/static/images/padlock.svg @@ -0,0 +1,46 @@ + + + + + + + + diff --git a/static/index.html b/static/index.html index 3ee9dc4..084b375 100644 --- a/static/index.html +++ b/static/index.html @@ -5,6 +5,8 @@ + + - - diff --git a/static/js/crypto.mjs b/static/js/crypto.mjs new file mode 100644 index 0000000..c81a3f8 --- /dev/null +++ b/static/js/crypto.mjs @@ -0,0 +1,69 @@ +export default class Crypto { + constructor(key) { + 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) + } + } +} diff --git a/static/js/key.mjs b/static/js/key.mjs new file mode 100644 index 0000000..73bd6a3 --- /dev/null +++ b/static/js/key.mjs @@ -0,0 +1,113 @@ +import 'preact/devtools' +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.retrieveKeys() + }//}}} + render({ nodeui }) {//{{{ + 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} />` + ) + + return html` +
+

Keys

+
+ ${keys} +
+
` + }//}}} + + retrieveKeys() {//{{{ + window._app.current.request('/key/retrieve', {}) + .then(res=>{ + this.props.nodeui.keys.value = res.Keys.map(keyData=>new Key(keyData)) + }) + .catch(window._app.current.responseError) + }//}}} +} + +export class Key { + constructor(data) {//{{{ + this.ID = data.ID + this.description = data.Description + this.unlockedKey = data.Key + this.key = null + + 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.unlockedKey) + 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)) + }//}}} +} + +export class KeyComponent extends Component { + render({ model }) {//{{{ + let status = '' + switch(model.status()) { + case 'locked': + status = html`
` + break + + case 'unlocked': + status = html`
` + break + } + + return html` +
this.toggle()}>${status}
+
this.toggle()}>${model.description}
+ ` + }//}}} + 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) + } + }//}}} +} + +// vim: foldmethod=marker diff --git a/static/js/lib/sjcl.js b/static/js/lib/sjcl.js new file mode 100644 index 0000000..b5a1021 --- /dev/null +++ b/static/js/lib/sjcl.js @@ -0,0 +1,2574 @@ +/** @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{ if(evt.state && evt.state.hasOwnProperty('nodeID')) this.goToNode(evt.state.nodeID, true) @@ -35,8 +37,8 @@ export class NodeUI extends Component { ).reverse()) let children = node.Children.sort((a,b)=>{ - if(a.Name.toLowerCase() > b.Name.toLowerCase()) return 1; - if(a.Name.toLowerCase() < b.Name.toLowerCase()) return -1; + if(a.Name.toLowerCase() > b.Name.toLowerCase()) return 1 + if(a.Name.toLowerCase() < b.Name.toLowerCase()) return -1 return 0 }).map(child=>html`
this.goToNode(child.ID)}>${child.Name}
@@ -44,23 +46,46 @@ export class NodeUI extends Component { let modified = '' if(this.props.app.nodeModified.value) - modified = 'modified'; + modified = 'modified' - let upload = ''; - if(this.upload.value) - upload = html`<${UploadUI} nodeui=${this} />` - let menu = ''; + // Page to display + let page = '' + switch(this.page.value) { + case 'node': + if(node.ID > 0) + page = html` + ${children.length > 0 ? html`
${children}
` : html``} +
${node.Name}
+ <${NodeContent} key=${node.ID} content=${node.Content} ref=${this.nodeContent} /> + <${NodeFiles} node=${this.node.value} /> + ` + break + + case 'upload': + page = html`<${UploadUI} nodeui=${this} />` + break + + case 'node-properties': + page = html`<${NodeProperties} nodeui=${this} />` + break + + case 'keys': + page = html`<${Keys} nodeui=${this} />` + break + } + + let menu = '' if(this.menu.value) - upload = html`<${Menu} nodeui=${this} />` + menu = html`<${Menu} nodeui=${this} />` return html` ${menu} - ${upload}
this.saveNode()}>
document.getElementById('app').classList.toggle('toggle-tree')} />
Notes
this.createNode(evt)}>+
+
this.showPage('keys')}>
@@ -68,14 +93,7 @@ export class NodeUI extends Component {
${crumbs}
- ${children.length > 0 ? html`
${children}
` : html``} - - ${node.ID > 0 ? html` -
${node.Name}
- <${NodeContent} key=${node.ID} content=${node.Content} ref=${this.nodeContent} /> - ` : html``} - - <${NodeFiles} node=${this.node.value} /> + ${page} ` }//}}} componentDidMount() {//{{{ @@ -91,6 +109,27 @@ export class NodeUI extends Component { keyHandler(evt) {//{{{ let handled = true switch(evt.key.toUpperCase()) { + case 'E': + if(evt.shiftKey && evt.altKey) + this.showPage('keys') + else + handled = false + break + + case 'N': + if(evt.shiftKey && evt.altKey) + this.createNode() + else + handled = false + break + + case 'P': + if(evt.shiftKey && evt.altKey) + this.showPage('node-properties') + else + handled = false + break + case 'S': if(evt.ctrlKey || (evt.shiftKey && evt.altKey)) this.saveNode() @@ -98,16 +137,10 @@ export class NodeUI extends Component { handled = false break - case 'N': - if((evt.ctrlKey && evt.AltKey) || (evt.shiftKey && evt.altKey)) - this.createNode() - else - handled = false - break case 'U': - if((evt.ctrlKey && evt.altKey) || (evt.shiftKey && evt.altKey)) - this.upload.value = true + if(evt.shiftKey && evt.altKey) + this.showPage('upload') else handled = false @@ -151,7 +184,7 @@ export class NodeUI extends Component { // Hide tree toggle, as this would be the next natural action to do manually anyway. // At least in mobile mode. - document.getElementById('app').classList.remove('toggle-tree'); + document.getElementById('app').classList.remove('toggle-tree') }) }//}}} createNode(evt) {//{{{ @@ -184,6 +217,10 @@ export class NodeUI extends Component { this.menu.value = false }) }//}}} + + showPage(pg) {//{{{ + this.page.value = pg + }//}}} } class NodeContent extends Component { @@ -342,12 +379,12 @@ export class Node { }) .then(blob=>{ let url = window.URL.createObjectURL(blob) - let a = document.createElement('a'); - a.href = url; - a.download = fname; - document.body.appendChild(a); // we need to append the element to the dom -> otherwise it will not work in firefox - a.click(); - a.remove(); //afterwards we remove the element again + let a = document.createElement('a') + a.href = url + a.download = fname + document.body.appendChild(a) // we need to append the element to the dom -> otherwise it will not work in firefox + a.click() + a.remove() //afterwards we remove the element again }) }//}}} } @@ -358,7 +395,8 @@ class Menu extends Component {
nodeui.menu.value = false}>
@@ -463,6 +501,19 @@ class UploadUI extends Component { request.send(formdata) }//}}} } - +class NodeProperties extends Component { + constructor(props) {//{{{ + super(props) + }//}}} + render({ nodeui }) {//{{{ + return html` +
nodeui.properties.value = false}>
+
+

Node properties

+ +
+ ` + }//}}} +} // vim: foldmethod=marker diff --git a/static/less/main.less b/static/less/main.less index 18972e1..9c3860c 100644 --- a/static/less/main.less +++ b/static/less/main.less @@ -23,29 +23,8 @@ html, body { } h1 { - color: @accent_1; -} - -.layout-crumbs { - grid-template-areas: - "header" - "crumbs" - "child-nodes" - "name" - "content" - "files" - "blank" - ; - grid-template-columns: 1fr; - grid-template-rows: - min-content /* header */ - min-content /* crumbs */ - min-content /* child-nodes */ - min-content /* name */ - min-content /* content */ - min-content /* files */ - 1fr; /* blank */ - #tree { display: none } + margin-top: 0px; + font-size: 1em; } #blackout { @@ -145,10 +124,23 @@ h1 { } } +#properties { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + color: #333; + background: #fff; + border: 2px solid #000; + padding: 16px; + z-index: 1025; +} + header { display: grid; grid-area: header; - grid-template-columns: min-content 1fr min-content min-content; + grid-template-columns: min-content 1fr repeat(3, min-content); align-items: center; //background: @accent_1; padding: 0px; @@ -186,6 +178,15 @@ header { cursor: pointer; } + .keys { + padding-right: 16px; + + img { + cursor: pointer; + height: 24px; + } + } + .menu { font-size: 1.25em; padding-right: 16px; @@ -415,6 +416,35 @@ header { } } +#keys { + padding: 32px; + color: #333; + + .key-list { + display: grid; + grid-template-columns: min-content 1fr; + grid-gap: 12px 12px; + align-items: end; + margin-top: 16px; + font-size: 0.85em; + + .status { + cursor: pointer; + img { + height: 32px; + } + + .locked { color: #a00 } + .unlocked { color: #0a0 } + } + + .description { + cursor: pointer; + padding-bottom: 4px; + } + } +} + .layout-tree {// {{{ display: grid; grid-template-areas: @@ -479,6 +509,27 @@ header { 1fr; /* blank */ #tree { display: none } }// }}} +.layout-keys { + display: grid; + grid-template-areas: + "header" + "keys" + ; + grid-template-columns: 1fr; + grid-template-rows: + min-content /* header */ + 1fr; /* blank */ + color: #fff; + height: 100%; + + #crumbs { display: none } + .child-nodes { display: none } + .node-name { display: none } + .grow-wrap { display: none } + #file-section { display: none } + #tree { display: none } + #keys { display: block } +} #app { .layout-tree();