diff --git a/bower.json b/bower.json index 809fd5323..8e6d5cb79 100644 --- a/bower.json +++ b/bower.json @@ -29,7 +29,7 @@ "json.sortify": "~2.1.0", "secure-fabric.js": "secure-v1.7.9", "hyperjson": "~1.4.0", - "chainpad-crypto": "^0.1.8", + "chainpad-crypto": "^0.2.0", "chainpad-listmap": "^0.5.0", "chainpad": "^5.1.0", "chainpad-netflux": "^0.7.0", diff --git a/customize.dist/loading.js b/customize.dist/loading.js index a2c1dcdfc..c08b82681 100644 --- a/customize.dist/loading.js +++ b/customize.dist/loading.js @@ -69,8 +69,9 @@ define([], function () { height: auto; margin-bottom: 2em; } -@media screen and (max-height: 450px) { - #cp-loading .cp-loading-cryptofist { +@media screen and (max-height: 500px) { + #cp-loading .cp-loading-logo { + display: none; } } #cp-loading-message { @@ -81,6 +82,43 @@ define([], function () { text-align: center; display: none; } +#cp-loading-password-prompt { + font-size: 18px; +} +#cp-loading-password-prompt .cp-password-error { + color: white; + background: #9e0000; + padding: 5px; + margin-bottom: 15px; +} +#cp-loading-password-prompt .cp-password-info { + text-align: left; + margin-bottom: 15px; +} +#cp-loading-password-prompt .cp-password-form { + display: flex; + justify-content: space-around; + flex-wrap: wrap; +} +#cp-loading-password-prompt .cp-password-form button, +#cp-loading-password-prompt .cp-password-form .cp-password-input { + background-color: #4591c4; + color: white; + border: 1px solid #4591c4; +} +#cp-loading-password-prompt .cp-password-form .cp-password-container { + flex-shrink: 1; + min-width: 0; +} +#cp-loading-password-prompt .cp-password-form input { + flex: 1; + padding: 0 5px; + min-width: 0; + text-overflow: ellipsis; +} +#cp-loading-password-prompt .cp-password-form button:hover { + background-color: #326599; +} #cp-loading .cp-loading-spinner-container { position: relative; height: 100px; @@ -114,6 +152,24 @@ define([], function () { max-width: 60%; display: inline-block; } +.cp-loading-progress { + width: 100%; + margin: 20px; +} +.cp-loading-progress p { + margin: 5px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.cp-loading-progress-bar { + height: 24px; + background: white; +} +.cp-loading-progress-bar-value { + height: 100%; + background: #5cb85c; +} */}).toString().slice(14, -3); var urlArgs = window.location.href.replace(/^.*\?([^\?]*)$/, function (all, x) { return x; }); var elem = document.createElement('div'); diff --git a/customize.dist/login.js b/customize.dist/login.js index af5a6391a..4d2855b16 100644 --- a/customize.dist/login.js +++ b/customize.dist/login.js @@ -154,7 +154,7 @@ define([ proxy.login_name = uname; proxy[Constants.displayNameKey] = uname; sessionStorage.createReadme = 1; - if (!shouldImport) { proxy.version = 5; } + if (!shouldImport) { proxy.version = 6; } Feedback.send('REGISTRATION', true); } else { Feedback.send('LOGIN', true); diff --git a/customize.dist/pages.js b/customize.dist/pages.js index 494a1926e..281bdc585 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -72,7 +72,7 @@ define([ ]) ]) ]), - h('div.cp-version-footer', "CryptPad v2.0.0 (Alpaca)") + h('div.cp-version-footer', "CryptPad v2.1.0 (Badger)") ]); }; diff --git a/customize.dist/src/less2/include/creation.less b/customize.dist/src/less2/include/creation.less index 7db3f5173..57509455e 100644 --- a/customize.dist/src/less2/include/creation.less +++ b/customize.dist/src/less2/include/creation.less @@ -145,16 +145,18 @@ max-height: 100px; } } + + input, select { + font-size: 14px; + border: 1px solid @colortheme_form-border; + height: 26px; + background-color: @colortheme_form-bg; + color: @colortheme_form-color; + } + .cp-creation-expire { .cp-creation-expire-picker { text-align: center; - input, select { - font-size: 14px; - border: 1px solid @colortheme_form-border; - height: 26px; - background-color: @colortheme_form-bg; - color: @colortheme_form-color; - } input { width: 50px; margin: 0 5px; @@ -172,6 +174,21 @@ } } } + .cp-creation-password { + .cp-creation-password-picker { + text-align: center; + width: 100%; + .cp-password-container { + input { + width: 150px; + padding: 0 5px; + } + label { + flex: unset; + } + } + } + } .cp-creation-settings { button { margin: 0; diff --git a/customize.dist/src/less2/include/framework.less b/customize.dist/src/less2/include/framework.less index 9dfab39f6..286870991 100644 --- a/customize.dist/src/less2/include/framework.less +++ b/customize.dist/src/less2/include/framework.less @@ -6,6 +6,7 @@ @import (once) './creation.less'; @import (once) './tippy.less'; @import (once) "./checkmark.less"; +@import (once) "./password-input.less"; .framework_main(@bg-color, @warn-color, @color) { .toolbar_main( @@ -18,11 +19,13 @@ .tokenfield_main(); .tippy_main(); .checkmark_main(20px); + .password_main(); .creation_main( @bg-color: @bg-color, @warn-color: @warn-color, @color: @color ); + font: @colortheme_app-font; } .framework_min_main( @@ -39,6 +42,8 @@ .alertify_main(); .tippy_main(); .checkmark_main(20px); + .password_main(); + font: @colortheme_app-font; } diff --git a/customize.dist/src/less2/include/password-input.less b/customize.dist/src/less2/include/password-input.less new file mode 100644 index 000000000..8836476fd --- /dev/null +++ b/customize.dist/src/less2/include/password-input.less @@ -0,0 +1,13 @@ +.password_main() { + .cp-password-container { + display: flex; + align-items: center; + input { + flex: 1; + min-width: 0; + } + label, .fa { + margin-left: 10px; + } + } +} diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 3454d6242..cf7ddcf8c 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -1081,6 +1081,7 @@ define(function () { out.creation_expireMonths = "Mois"; out.creation_expire1 = "Un pad illimité ne sera pas supprimé du serveur à moins que son propriétaire ne le décide."; out.creation_expire2 = "Un pad à durée de vie sera supprimé automatiquement du serveur et du CryptDrive des utilisateurs lorsque cette durée sera dépassée."; + out.creation_password = "Ajouter un mot de passe"; out.creation_noTemplate = "Pas de modèle"; out.creation_newTemplate = "Nouveau modèle"; out.creation_create = "Créer"; @@ -1092,12 +1093,20 @@ define(function () { out.creation_ownedByOther = "Appartient à un autre utilisateur"; out.creation_noOwner = "Pas de propriétaire"; out.creation_expiration = "Date d'expiration"; + out.creation_passwordValue = "Mot de passe"; out.creation_propertiesTitle = "Disponibilité"; out.creation_appMenuName = "Mode avancé (Ctrl + E)"; out.creation_newPadModalDescription = "Cliquez sur un type de pad pour le créer. Vous pouvez aussi appuyer sur Tab pour sélectionner un type et appuyer sur Entrée pour valider."; out.creation_newPadModalDescriptionAdvanced = "Cochez la case si vous souhaitez voir l'écran de création de pads (pour les pads avec propriétaire ou à durée de vie). Vous pouvez appuyer sur Espace pour changer sa valeur."; out.creation_newPadModalAdvanced = "Afficher l'écran de création de pads"; + // Password prompt on the loadind screen + out.password_info = "Le pad auquel vous essayez d'accéder est protégé par un mot de passe. Entrez le bon mot de passe pour accéder à son contenu."; + out.password_error = "Pad introuvable !
Cette erreur peut provenir de deux facteurs. Soit le mot de passe est faux, soit le pad a été supprimé du serveur."; + out.password_placeholder = "Tapez le mot de passe ici..."; + out.password_submit = "Valider"; + out.password_show = "Afficher"; + // New share modal out.share_linkCategory = "Partage"; out.share_linkAccess = "Droits d'accès"; @@ -1111,5 +1120,12 @@ define(function () { out.share_embedCategory = "Intégration"; out.share_mediatagCopy = "Copier le mediatag"; + // Loading info + out.loading_pad_1 = "Initialisation du pad"; + out.loading_pad_2 = "Chargement du contenu du pad"; + out.loading_drive_1 = "Chargement des données"; + out.loading_drive_2 = "Mise à jour du format des données"; + out.loading_drive_3 = "Vérification de l'intégrité des données"; + return out; }); diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index ddaa1158f..18e295a4c 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -1127,6 +1127,7 @@ define(function () { out.creation_expireMonths = "Month(s)"; out.creation_expire1 = "An unlimited pad will not be removed from the server until its owner deletes it."; out.creation_expire2 = "An expiring pad has a set lifetime, after which it will be automatically removed from the server and other users' CryptDrives."; + out.creation_password = "Add a password"; out.creation_noTemplate = "No template"; out.creation_newTemplate = "New template"; out.creation_create = "Create"; @@ -1138,12 +1139,20 @@ define(function () { out.creation_ownedByOther = "Owned by another user"; out.creation_noOwner = "No owner"; out.creation_expiration = "Expiration time"; + out.creation_passwordValue = "Password"; out.creation_propertiesTitle = "Availability"; out.creation_appMenuName = "Advanced mode (Ctrl + E)"; out.creation_newPadModalDescription = "Click on a pad type to create it. You can also press Tab to select the type and press Enter to confirm."; out.creation_newPadModalDescriptionAdvanced = "You can check the box (or press Space to change its value) if you want to display the pad creation screen (for owned pads, expiring pads, etc.)."; out.creation_newPadModalAdvanced = "Display the pad creation screen"; + // Password prompt on the loadind screen + out.password_info = "The pad you're tyring to open is protected with a password. Enter the correct password to access its content."; + out.password_error = "Pad not found!
This error can be caused by two factors: either the password in invalid, or the pad has been deleted from the server."; + out.password_placeholder = "Type the password here..."; + out.password_submit = "Submit"; + out.password_show = "Show"; + // New share modal out.share_linkCategory = "Share link"; out.share_linkAccess = "Access rights"; @@ -1157,6 +1166,12 @@ define(function () { out.share_embedCategory = "Embed"; out.share_mediatagCopy = "Copy mediatag to clipboard"; + // Loading info + out.loading_pad_1 = "Initializing pad"; + out.loading_pad_2 = "Loading pad content"; + out.loading_drive_1 = "Loading data"; + out.loading_drive_2 = "Updating data format"; + out.loading_drive_3 = "Verifying data integrity"; return out; }); diff --git a/package.json b/package.json index c58111e31..b74dff06d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cryptpad", "description": "realtime collaborative visual editor with zero knowlege server", - "version": "2.0.0", + "version": "2.1.0", "license": "AGPL-3.0-or-later", "dependencies": { "chainpad-server": "^2.0.0", diff --git a/www/assert/main.js b/www/assert/main.js index eb470eb35..657488b81 100644 --- a/www/assert/main.js +++ b/www/assert/main.js @@ -132,6 +132,40 @@ define([ strungJSON(orig); }); + HTML_list.forEach(function (sel) { + var el = $(sel)[0]; + + var pred = function (el) { + if (el.nodeName === 'DIV') { + return true; + } + }; + + var filter = function (x) { + console.log(x); + if (x[1]['class']) { + x[1]['class'] = x[1]['class'].replace(/cke/g, ''); + } + return x; + }; + + assert(function (cb) { + // FlatDOM output + var map = Flat.fromDOM(el, pred, filter); + + // Hyperjson output + var hj = Hyperjson.fromDOM(el, pred, filter); + + var x = Flat.toDOM(map); + var y = Hyperjson.toDOM(hj); + + console.error(x.outerHTML); + console.error(y.outerHTML); + + cb(x.outerHTML === y.outerHTML); + }, "Test equality of FlatDOM and HyperJSON"); + }); + // check that old hashes parse correctly assert(function (cb) { //if (1) { return cb(true); } // TODO(cjd): This is a test failure which is a known bug @@ -223,6 +257,33 @@ define([ hd.type === 'invite'); }, "test support for invite urls"); + // test support for V2 + assert(function (cb) { + var parsed = Hash.parsePadUrl('/pad/#/2/pad/edit/oRE0oLCtEXusRDyin7GyLGcS/'); + var secret = Hash.getSecrets('pad', '/2/pad/edit/oRE0oLCtEXusRDyin7GyLGcS/'); + return cb(parsed.hashData.version === 2 && + parsed.hashData.mode === "edit" && + parsed.hashData.type === "pad" && + parsed.hashData.key === "oRE0oLCtEXusRDyin7GyLGcS" && + secret.channel === "d8d51b4aea863f3f050f47f8ad261753" && + window.nacl.util.encodeBase64(secret.keys.cryptKey) === "0Ts1M6VVEozErV2Nx/LTv6Im5SCD7io2LlhasyyBPQo=" && + secret.keys.validateKey === "f5A1FM9Gp55tnOcM75RyHD1oxBG9ZPh9WDA7qe2Fvps=" && + !parsed.hashData.present); + }, "test support for version 2 hash failed to parse"); + assert(function (cb) { + var parsed = Hash.parsePadUrl('/pad/#/2/pad/edit/HGu0tK2od-2BBnwAz2ZNS-t4/p/embed'); + var secret = Hash.getSecrets('pad', '/2/pad/edit/HGu0tK2od-2BBnwAz2ZNS-t4/p/embed', 'pewpew'); + return cb(parsed.hashData.version === 2 && + parsed.hashData.mode === "edit" && + parsed.hashData.type === "pad" && + parsed.hashData.key === "HGu0tK2od-2BBnwAz2ZNS-t4" && + secret.channel === "3fb6dc93807d903aff390b5f798c92c9" && + window.nacl.util.encodeBase64(secret.keys.cryptKey) === "EeCkGJra8eJgVu7v4Yl2Hc3yUjrgpKpxr0Lcc3bSWVs=" && + secret.keys.validateKey === "WGkBczJf2V6vQZfAScz8V1KY6jKdoxUCckrD+E75gGE=" && + parsed.hashData.embed && + parsed.hashData.password); + }, "test support for password in version 2 hash failed to parse"); + assert(function (cb) { var url = '/pad/?utm_campaign=new_comment&utm_medium=email&utm_source=thread_mailer#/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI/'; var secret = Hash.parsePadUrl(url); diff --git a/www/code/inner.js b/www/code/inner.js index c2e5851bf..0a6b11abc 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -334,6 +334,7 @@ define([ //var cursor = editor.getCursor(); //var cleanName = data.name.replace(/[\[\]]/g, ''); //var text = '!['+cleanName+']('+data.url+')'; + // PASSWORD_FILES var parsed = Hash.parsePadUrl(data.url); var hexFileName = Util.base64ToHex(parsed.hashData.channel); var src = '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName; diff --git a/www/common/common-hash.js b/www/common/common-hash.js index 870b19dfe..5ba9ec5b3 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -19,7 +19,50 @@ define([ .decodeUTF8(JSON.stringify(list)))); }; - var getEditHashFromKeys = Hash.getEditHashFromKeys = function (chanKey, keys) { + var getEditHashFromKeys = Hash.getEditHashFromKeys = function (secret) { + var version = secret.version; + var data = secret.keys; + if (version === 0) { + return secret.channel + secret.key; + } + if (version === 1) { + if (!data.editKeyStr) { return; } + return '/1/edit/' + hexToBase64(secret.channel) + + '/' + Crypto.b64RemoveSlashes(data.editKeyStr) + '/'; + } + if (version === 2) { + if (!data.editKeyStr) { return; } + var pass = secret.password ? 'p/' : ''; + return '/2/' + secret.type + '/edit/' + Crypto.b64RemoveSlashes(data.editKeyStr) + '/' + pass; + } + }; + var getViewHashFromKeys = Hash.getViewHashFromKeys = function (secret) { + var version = secret.version; + var data = secret.keys; + if (version === 0) { return; } + if (version === 1) { + if (!data.viewKeyStr) { return; } + return '/1/view/' + hexToBase64(secret.channel) + + '/'+Crypto.b64RemoveSlashes(data.viewKeyStr)+'/'; + } + if (version === 2) { + if (!data.viewKeyStr) { return; } + var pass = secret.password ? 'p/' : ''; + return '/2/' + secret.type + '/view/' + Crypto.b64RemoveSlashes(data.viewKeyStr) + '/' + pass; + } + }; + var getFileHashFromKeys = Hash.getFileHashFromKeys = function (secret) { + var version = secret.version; + var data = secret.keys; + if (version === 0) { return; } + if (version === 1) { + return '/1/' + hexToBase64(secret.channel) + '/' + + Crypto.b64RemoveSlashes(data.fileKeyStr) + '/'; + } + }; + + // V1 + /*var getEditHashFromKeys = Hash.getEditHashFromKeys = function (chanKey, keys) { if (typeof keys === 'string') { return chanKey + keys; } @@ -34,7 +77,7 @@ define([ }; var getFileHashFromKeys = Hash.getFileHashFromKeys = function (fileKey, cryptKey) { return '/1/' + hexToBase64(fileKey) + '/' + Crypto.b64RemoveSlashes(cryptKey) + '/'; - }; + };*/ Hash.getUserHrefFromKeys = function (origin, username, pubkey) { return origin + '/user/#/1/' + username + '/' + pubkey.replace(/\//g, '-'); }; @@ -43,6 +86,24 @@ define([ return s.replace(/\/+/g, '/'); }; + Hash.createChannelId = function () { + var id = uint8ArrayToHex(Crypto.Nacl.randomBytes(16)); + if (id.length !== 32 || /[^a-f0-9]/.test(id)) { + throw new Error('channel ids must consist of 32 hex characters'); + } + return id; + }; + + Hash.createRandomHash = function (type, password) { + var cryptor = Crypto.createEditCryptor2(void 0, void 0, password); + return getEditHashFromKeys({ + password: Boolean(password), + version: 2, + type: type, + keys: { editKeyStr: cryptor.editKeyStr } + }); + }; + /* Version 0 /pad/#67b8385b07352be53e40746d2be6ccd7XAYSuJYYqa9NfmInyHci7LNy @@ -56,25 +117,56 @@ Version 1 var hashArr = fixDuplicateSlashes(hash).split('/'); if (['media', 'file', 'user', 'invite'].indexOf(type) === -1) { parsed.type = 'pad'; - if (hash.slice(0,1) !== '/' && hash.length >= 56) { + if (hash.slice(0,1) !== '/' && hash.length >= 56) { // Version 0 // Old hash parsed.channel = hash.slice(0, 32); parsed.key = hash.slice(32, 56); parsed.version = 0; + parsed.getHash = function () { return hash; }; return parsed; } - if (hashArr[1] && hashArr[1] === '1') { + var options; + if (hashArr[1] && hashArr[1] === '1') { // Version 1 parsed.version = 1; parsed.mode = hashArr[2]; parsed.channel = hashArr[3]; - parsed.key = hashArr[4].replace(/-/g, '/'); - var options = hashArr.slice(5); + parsed.key = Crypto.b64AddSlashes(hashArr[4]); + + options = hashArr.slice(5); parsed.present = options.indexOf('present') !== -1; parsed.embed = options.indexOf('embed') !== -1; + + parsed.getHash = function (opts) { + var hash = hashArr.slice(0, 5).join('/') + '/'; + if (opts.embed) { hash += 'embed/'; } + if (opts.present) { hash += 'present/'; } + return hash; + }; + return parsed; + } + if (hashArr[1] && hashArr[1] === '2') { // Version 2 + parsed.version = 2; + parsed.app = hashArr[2]; + parsed.mode = hashArr[3]; + parsed.key = hashArr[4]; + + options = hashArr.slice(5); + parsed.password = options.indexOf('p') !== -1; + parsed.present = options.indexOf('present') !== -1; + parsed.embed = options.indexOf('embed') !== -1; + + parsed.getHash = function (opts) { + var hash = hashArr.slice(0, 5).join('/') + '/'; + if (parsed.password) { hash += 'p/'; } + if (opts.embed) { hash += 'embed/'; } + if (opts.present) { hash += 'present/'; } + return hash; + }; return parsed; } return parsed; } + parsed.getHash = function () { return hashArr.join('/'); }; if (['media', 'file'].indexOf(type) !== -1) { parsed.type = 'file'; if (hashArr[1] && hashArr[1] === '1') { @@ -125,17 +217,9 @@ Version 1 url += ret.type + '/'; if (!ret.hashData) { return url; } if (ret.hashData.type !== 'pad') { return url + '#' + ret.hash; } - if (ret.hashData.version !== 1) { return url + '#' + ret.hash; } - url += '#/' + ret.hashData.version + - '/' + ret.hashData.mode + - '/' + ret.hashData.channel.replace(/\//g, '-') + - '/' + ret.hashData.key.replace(/\//g, '-') +'/'; - if (options.embed) { - url += 'embed/'; - } - if (options.present) { - url += 'present/'; - } + if (ret.hashData.version === 0) { return url + '#' + ret.hash; } + var hash = ret.hashData.getHash(options); + url += '#' + hash; return url; }; @@ -153,12 +237,13 @@ Version 1 return ''; }); idx = href.indexOf('/#'); + if (idx === -1) { return ret; } ret.hash = href.slice(idx + 2); ret.hashData = parseTypeHash(ret.type, ret.hash); return ret; }; - var getRelativeHref = Hash.getRelativeHref = function (href) { + Hash.getRelativeHref = function (href) { if (!href) { return; } if (href.indexOf('#') === -1) { return; } var parsed = parsePadUrl(href); @@ -170,11 +255,13 @@ Version 1 * - no argument: use the URL hash or create one if it doesn't exist * - secretHash provided: use secretHash to find the keys */ - Hash.getSecrets = function (type, secretHash) { + Hash.getSecrets = function (type, secretHash, password) { var secret = {}; var generate = function () { - secret.keys = Crypto.createEditCryptor(); - secret.key = Crypto.createEditCryptor().editKeyStr; + secret.keys = Crypto.createEditCryptor2(void 0, void 0, password); + secret.channel = base64ToHex(secret.keys.chanId); + secret.version = 2; + secret.type = type; }; if (!secretHash && !window.location.hash) { //!/#/.test(window.location.href)) { generate(); @@ -191,7 +278,6 @@ Version 1 parsed = pHref.hashData; hash = pHref.hash; } - //var parsed = parsePadUrl(window.location.href); //var hash = secretHash || window.location.hash.slice(1); if (hash.length === 0) { generate(); @@ -203,9 +289,10 @@ Version 1 // Old hash secret.channel = parsed.channel; secret.key = parsed.key; - } - else if (parsed.version === 1) { + secret.version = 0; + } else if (parsed.version === 1) { // New hash + secret.version = 1; if (parsed.type === "pad") { secret.channel = base64ToHex(parsed.channel); if (parsed.mode === 'edit') { @@ -229,49 +316,63 @@ Version 1 // version 2 hashes are to be used for encrypted blobs throw new Error("User hashes can't be opened (yet)"); } + } else if (parsed.version === 2) { + // New hash + secret.version = 2; + secret.type = type; + secret.password = password; + if (parsed.type === "pad") { + if (parsed.mode === 'edit') { + secret.keys = Crypto.createEditCryptor2(parsed.key, void 0, password); + secret.channel = base64ToHex(secret.keys.chanId); + secret.key = secret.keys.editKeyStr; + if (secret.channel.length !== 32 || secret.key.length !== 24) { + throw new Error("The channel key and/or the encryption key is invalid"); + } + } + else if (parsed.mode === 'view') { + secret.keys = Crypto.createViewCryptor2(parsed.key, password); + secret.channel = base64ToHex(secret.keys.chanId); + if (secret.channel.length !== 32) { + throw new Error("The channel key is invalid"); + } + } + } else if (parsed.type === "file") { + throw new Error("File hashes should be version 1"); + } else if (parsed.type === "user") { + throw new Error("User hashes can't be opened (yet)"); + } } } return secret; }; - Hash.getHashes = function (channel, secret) { + Hash.getHashes = function (secret) { var hashes = {}; - if (!secret.keys) { + secret = JSON.parse(JSON.stringify(secret)); + + if (!secret.keys && !secret.key) { console.error('e'); return hashes; + } else if (!secret.keys) { + secret.keys = {}; } - if (secret.keys.editKeyStr) { - hashes.editHash = getEditHashFromKeys(channel, secret.keys); + + if (secret.keys.editKeyStr || (secret.version === 0 && secret.key)) { + hashes.editHash = getEditHashFromKeys(secret); } if (secret.keys.viewKeyStr) { - hashes.viewHash = getViewHashFromKeys(channel, secret.keys); + hashes.viewHash = getViewHashFromKeys(secret); } if (secret.keys.fileKeyStr) { - hashes.fileHash = getFileHashFromKeys(channel, secret.keys.fileKeyStr); + hashes.fileHash = getFileHashFromKeys(secret); } return hashes; }; - var createChannelId = Hash.createChannelId = function () { - var id = uint8ArrayToHex(Crypto.Nacl.randomBytes(16)); - if (id.length !== 32 || /[^a-f0-9]/.test(id)) { - throw new Error('channel ids must consist of 32 hex characters'); - } - return id; - }; - - Hash.createRandomHash = function () { - // 16 byte channel Id - var channelId = Util.hexToBase64(createChannelId()); - // 18 byte encryption key - var key = Crypto.b64RemoveSlashes(Crypto.rand64(18)); - return '/1/edit/' + [channelId, key].join('/') + '/'; - }; - // STORAGE - Hash.findWeaker = function (href, recents) { - var rHref = href || getRelativeHref(window.location.href); - var parsed = parsePadUrl(rHref); + Hash.findWeaker = function (href, channel, recents) { + var parsed = parsePadUrl(href); if (!parsed.hash) { return false; } var weaker; Object.keys(recents).some(function (id) { @@ -279,6 +380,8 @@ Version 1 var p = parsePadUrl(pad.href); if (p.type !== parsed.type) { return; } // Not the same type if (p.hash === parsed.hash) { return; } // Same hash, not stronger + if (channel !== pad.channel) { return; } // Not the same channel + var pHash = p.hashData; var parsedHash = parsed.hashData; if (!parsedHash || !pHash) { return; } @@ -287,18 +390,16 @@ Version 1 if (pHash.type !== 'pad' && parsedHash.type !== 'pad') { return; } if (pHash.version !== parsedHash.version) { return; } - if (pHash.channel !== parsedHash.channel) { return; } if (pHash.mode === 'view' && parsedHash.mode === 'edit') { - weaker = pad.href; + weaker = pad; return true; } return; }); return weaker; }; - var findStronger = Hash.findStronger = function (href, recents) { - var rHref = href || getRelativeHref(window.location.href); - var parsed = parsePadUrl(rHref); + Hash.findStronger = function (href, channel, recents) { + var parsed = parsePadUrl(href); if (!parsed.hash) { return false; } // We can't have a stronger hash if we're already in edit mode if (parsed.hashData && parsed.hashData.mode === 'edit') { return; } @@ -308,6 +409,8 @@ Version 1 var p = parsePadUrl(pad.href); if (p.type !== parsed.type) { return; } // Not the same type if (p.hash === parsed.hash) { return; } // Same hash, not stronger + if (channel !== pad.channel) { return; } // Not the same channel + var pHash = p.hashData; var parsedHash = parsed.hashData; if (!parsedHash || !pHash) { return; } @@ -316,37 +419,20 @@ Version 1 if (pHash.type !== 'pad' && parsedHash.type !== 'pad') { return; } if (pHash.version !== parsedHash.version) { return; } - if (pHash.channel !== parsedHash.channel) { return; } if (pHash.mode === 'edit' && parsedHash.mode === 'view') { - stronger = pad.href; + stronger = pad; return true; } return; }); return stronger; }; - Hash.isNotStrongestStored = function (href, recents) { - return findStronger(href, recents); - }; - Hash.hrefToHexChannelId = function (href) { + Hash.hrefToHexChannelId = function (href, password) { var parsed = Hash.parsePadUrl(href); if (!parsed || !parsed.hash) { return; } - - parsed = parsed.hashData; - if (parsed.version === 0) { - return parsed.channel; - } else if (parsed.version !== 1 && parsed.version !== 2) { - console.error("parsed href had no version"); - console.error(parsed); - return; - } - - var channel = parsed.channel; - if (!channel) { return; } - - var hex = base64ToHex(channel); - return hex; + var secret = Hash.getSecrets(parsed.type, parsed.hash, password); + return secret.channel; }; Hash.getBlobPathFromHex = function (id) { diff --git a/www/common/common-interface.js b/www/common/common-interface.js index ea2773e37..7f318dfae 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -513,6 +513,50 @@ define([ Alertify.error(Util.fixHTML(msg)); }; + UI.passwordInput = function (opts, displayEye) { + opts = opts || {}; + var attributes = merge({ + type: 'password' + }, opts); + + var input = h('input.cp-password-input', attributes); + var reveal = UI.createCheckbox('cp-password-reveal', Messages.password_show); + var eye = h('span.fa.fa-eye.cp-password-reveal'); + + $(reveal).find('input').on('change', function () { + if($(this).is(':checked')) { + $(input).prop('type', 'text'); + $(input).focus(); + return; + } + $(input).prop('type', 'password'); + $(input).focus(); + }); + + $(eye).mousedown(function () { + $(input).prop('type', 'text'); + $(input).focus(); + }).mouseup(function(){ + $(input).prop('type', 'password'); + $(input).focus(); + }).mouseout(function(){ + $(input).prop('type', 'password'); + $(input).focus(); + }); + if (displayEye) { + $(reveal).hide(); + } else { + $(eye).hide(); + } + + return h('span.cp-password-container', [ + input, + reveal, + eye + ]); + }; + + /* * spinner */ @@ -546,6 +590,11 @@ define([ var rdm = Math.floor(Math.random() * keys.length); return Messages.tips[keys[rdm]]; };*/ + var loading = { + error: false, + driveState: 0, + padState: 0 + }; UI.addLoadingScreen = function (config) { config = config || {}; var loadingText = config.loadingText; @@ -554,11 +603,21 @@ define([ $loading.css('display', ''); $loading.removeClass('cp-loading-hidden'); $('.cp-loading-spinner-container').show(); + if (!config.noProgress && !$loading.find('.cp-loading-progress').length) { + var progress = h('div.cp-loading-progress', [ + h('p.cp-loading-progress-drive'), + h('p.cp-loading-progress-pad') + ]); + $loading.find('.cp-loading-container').append(progress); + } else if (config.noProgress) { + $loading.find('.cp-loading-progress').remove(); + } if (loadingText) { - $('#' + LOADING).find('p').show().text(loadingText); + $('#' + LOADING).find('#cp-loading-message').show().text(loadingText); } else { - $('#' + LOADING).find('p').hide().text(''); + $('#' + LOADING).find('#cp-loading-message').hide().text(''); } + loading.error = false; }; if ($('#' + LOADING).length) { todo(); @@ -567,6 +626,58 @@ define([ todo(); } }; + UI.updateLoadingProgress = function (data, isDrive) { + var $loading = $('#' + LOADING); + if (!$loading.length || loading.error) { return; } + var $progress; + if (isDrive) { + // Drive state + if (loading.driveState === -1) { return; } // Already loaded + $progress = $loading.find('.cp-loading-progress-drive'); + if (!$progress.length) { return; } // Can't find the box to display data + + // If state is -1, remove the box, drive is loaded + if (data.state === -1) { + loading.driveState = -1; + $progress.remove(); + } else { + if (data.state < loading.driveState) { return; } // We should not display old data + // Update the current state + loading.driveState = data.state; + data.progress = data.progress || 100; + data.msg = Messages['loading_drive_'+data.state] || ''; + $progress.html(data.msg); + if (data.progress) { + $progress.append(h('div.cp-loading-progress-bar', [ + h('div.cp-loading-progress-bar-value', {style: 'width:'+data.progress+'%;'}) + ])); + } + } + } else { + // Pad state + if (loading.padState === -1) { return; } // Already loaded + $progress = $loading.find('.cp-loading-progress-pad'); + if (!$progress.length) { return; } // Can't find the box to display data + + // If state is -1, remove the box, pad is loaded + if (data.state === -1) { + loading.padState = -1; + $progress.remove(); + } else { + if (data.state < loading.padState) { return; } // We should not display old data + // Update the current state + loading.padState = data.state; + data.progress = data.progress || 100; + data.msg = Messages['loading_pad_'+data.state] || ''; + $progress.html(data.msg); + if (data.progress) { + $progress.append(h('div.cp-loading-progress-bar', [ + h('div.cp-loading-progress-bar-value', {style: 'width:'+data.progress+'%;'}) + ])); + } + } + } + }; UI.removeLoadingScreen = function (cb) { // Release the test blocker, hopefully every test has been registered. // This test is created in sframe-boot2.js @@ -575,6 +686,7 @@ define([ $('#' + LOADING).addClass("cp-loading-hidden"); setTimeout(cb, 750); + loading.error = false; var $tip = $('#cp-loading-tip').css('top', '') // loading.less sets transition-delay: $wait-time // and transition: opacity $fadeout-time @@ -588,18 +700,27 @@ define([ // jquery.fadeout can get stuck }; UI.errorLoadingScreen = function (error, transparent, exitable) { - if (!$('#' + LOADING).is(':visible') || $('#' + LOADING).hasClass('cp-loading-hidden')) { + var $loading = $('#' + LOADING); + if (!$loading.is(':visible') || $loading.hasClass('cp-loading-hidden')) { UI.addLoadingScreen({hideTips: true}); } + loading.error = true; + $loading.find('.cp-loading-progress').remove(); $('.cp-loading-spinner-container').hide(); $('#cp-loading-tip').remove(); - if (transparent) { $('#' + LOADING).css('opacity', 0.9); } - $('#' + LOADING).find('p').show().html(error || Messages.error); + if (transparent) { $loading.css('opacity', 0.9); } + var $error = $loading.find('#cp-loading-message').show(); + if (error instanceof Element) { + $error.html('').append(error); + } else { + $error.html(error || Messages.error); + } if (exitable) { $(window).focus(); $(window).keydown(function (e) { if (e.which === 27) { - $('#' + LOADING).hide(); + $loading.hide(); + loading.error = false; if (typeof(exitable) === "function") { exitable(); } } }); @@ -659,12 +780,14 @@ define([ } }, //arrowType: 'round', + dynamicTitle: true, arrowTransform: 'scale(2)', zIndex: 100000001 }); UI.addTooltips = function () { var MutationObserver = window.MutationObserver; var addTippy = function (i, el) { + if (el._tippy) { return; } if (el.nodeName === 'IFRAME') { return; } var opts = { distance: 15 diff --git a/www/common/common-messaging.js b/www/common/common-messaging.js index 13d132219..b5a12da0b 100644 --- a/www/common/common-messaging.js +++ b/www/common/common-messaging.js @@ -99,7 +99,7 @@ define([ try { var parsed = Hash.parsePadUrl(window.location.href); if (!parsed.hashData) { return; } - var chan = parsed.hashData.channel; + var chan = Hash.hrefToHexChannelId(window.location.href); // Decrypt var keyStr = parsed.hashData.key; var cryptor = Crypto.createEditCryptor(keyStr); @@ -113,7 +113,7 @@ define([ if (!decryptMsg) { return; } // Parse msg = JSON.parse(decryptMsg); - if (msg[1] !== parsed.hashData.channel) { return; } + if (msg[1] !== chan) { return; } var msgData = msg[2]; var msgStr; if (msg[0] === "FRIEND_REQ") { @@ -199,7 +199,7 @@ define([ var parsed = Hash.parsePadUrl(data.href); if (!parsed.hashData) { return; } // Message - var chan = parsed.hashData.channel; + var chan = Hash.hrefToHexChannelId(data.href); var myData = createData(cfg.proxy); var msg = ["FRIEND_REQ", chan, myData]; // Encryption diff --git a/www/common/common-thumbnail.js b/www/common/common-thumbnail.js index 455b095c9..ab6d3e6d4 100644 --- a/www/common/common-thumbnail.js +++ b/www/common/common-thumbnail.js @@ -205,7 +205,7 @@ define([ if (content === oldThumbnailState) { return; } oldThumbnailState = content; Thumb.fromDOM(opts, function (err, b64) { - Thumb.setPadThumbnail(common, opts.href, b64); + Thumb.setPadThumbnail(common, opts.href, null, b64); }); }; var nafa = Util.notAgainForAnother(mkThumbnail, Thumb.UPDATE_INTERVAL); @@ -240,20 +240,22 @@ define([ Thumb.addThumbnail = function(thumb, $span, cb) { return addThumbnail(null, thumb, $span, cb); }; - var getKey = function (href) { - var parsed = Hash.parsePadUrl(href); - return 'thumbnail-' + parsed.type + '-' + parsed.hashData.channel; + var getKey = function (type, channel) { + return 'thumbnail-' + type + '-' + channel; }; - Thumb.setPadThumbnail = function (common, href, b64, cb) { + Thumb.setPadThumbnail = function (common, href, channel, b64, cb) { cb = cb || function () {}; - var k = getKey(href); + var parsed = Hash.parsePadUrl(href); + channel = channel || common.getMetadataMgr().getPrivateData().channel; + var k = getKey(parsed.type, channel); common.setThumbnail(k, b64, cb); }; - Thumb.displayThumbnail = function (common, href, $container, cb) { + Thumb.displayThumbnail = function (common, href, channel, $container, cb) { cb = cb || function () {}; var parsed = Hash.parsePadUrl(href); - var k = getKey(href); + var k = getKey(parsed.type, channel); var whenNewThumb = function () { + // PASSWORD_FILES var secret = Hash.getSecrets('file', parsed.hash); var hexFileName = Util.base64ToHex(secret.channel); var src = Hash.getBlobPathFromHex(hexFileName); @@ -270,7 +272,7 @@ define([ if (!v) { v = 'EMPTY'; } - Thumb.setPadThumbnail(common, href, v, function (err) { + Thumb.setPadThumbnail(common, href, hexFileName, v, function (err) { if (!metadata.thumbnail) { return; } addThumbnail(err, metadata.thumbnail, $container, cb); }); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 2b9487a73..e085ea3a3 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -60,6 +60,10 @@ define([ var getPropertiesData = function (common, cb) { var data = {}; NThen(function (waitFor) { + common.getPadAttribute('password', waitFor(function (err, val) { + data.password = val; + })); + }).nThen(function (waitFor) { common.getPadAttribute('href', waitFor(function (err, val) { var base = common.getMetadataMgr().getPrivateData().origin; @@ -71,15 +75,19 @@ define([ // We're not in a read-only pad data.href = base + val; + // Get Read-only href if (parsed.hashData.type !== "pad") { return; } var i = data.href.indexOf('#') + 1; var hBase = data.href.slice(0, i); - var hrefsecret = Hash.getSecrets(parsed.type, parsed.hash); + var hrefsecret = Hash.getSecrets(parsed.type, parsed.hash, data.password); if (!hrefsecret.keys) { return; } - var viewHash = Hash.getViewHashFromKeys(hrefsecret.channel, hrefsecret.keys); + var viewHash = Hash.getViewHashFromKeys(hrefsecret); data.roHref = hBase + viewHash; })); + common.getPadAttribute('channel', waitFor(function (err, val) { + data.channel = val; + })); common.getPadAttribute('atime', waitFor(function (err, val) { data.atime = val; })); @@ -135,6 +143,22 @@ define([ $d.append(UI.dialog.selectable(expire, { id: 'cp-app-prop-expire', })); + + if (typeof data.password !== "undefined") { + $('