From c7e08fedfbda97ad331e566c149c1235bb294a18 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 25 May 2018 18:00:10 +0200 Subject: [PATCH] Password-protected files --- www/code/inner.js | 11 ++--- www/common/common-hash.js | 57 ++++++++++++++++++++++---- www/common/common-thumbnail.js | 10 ++--- www/common/common-ui-elements.js | 4 +- www/common/cryptpad-common.js | 1 - www/common/diffMarked.js | 12 ++++-- www/common/migrate-user-object.js | 9 +--- www/common/outer/async-store.js | 2 +- www/common/outer/upload.js | 23 +++++------ www/common/outer/userObject.js | 7 +--- www/common/sframe-common-codemirror.js | 12 ++---- www/common/sframe-common.js | 12 +++--- www/common/userObject.js | 1 - www/drive/inner.js | 2 +- www/file/inner.js | 16 +++----- www/filepicker/inner.js | 18 ++++---- www/pad/inner.js | 8 ++-- www/slide/inner.js | 8 ++-- 18 files changed, 117 insertions(+), 96 deletions(-) diff --git a/www/code/inner.js b/www/code/inner.js index 0a6b11abc..afb4cdcd0 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -331,14 +331,11 @@ define([ dropArea: $('.CodeMirror'), body: $('body'), onUploaded: function (ev, data) { - //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; - var mt = ''; + var secret = Hash.getSecrets('file', parsed.hash, data.password); + var src = Hash.getBlobPathFromHex(secret.channel); + var key = Hash.encodeBase64(secret.keys.cryptKey); + var mt = ''; editor.replaceSelection(mt); } }; diff --git a/www/common/common-hash.js b/www/common/common-hash.js index 3aaa7719b..2b75b0e8e 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -11,6 +11,7 @@ define([ var uint8ArrayToHex = Util.uint8ArrayToHex; var hexToBase64 = Util.hexToBase64; var base64ToHex = Util.base64ToHex; + Hash.encodeBase64 = Nacl.util.encodeBase64; // This implementation must match that on the server // it's used for a checksum @@ -59,6 +60,11 @@ define([ return '/1/' + hexToBase64(secret.channel) + '/' + Crypto.b64RemoveSlashes(data.fileKeyStr) + '/'; } + if (version === 2) { + if (!data.fileKeyStr) { return; } + var pass = secret.password ? 'p/' : ''; + return '/2/' + secret.type + '/' + Crypto.b64RemoveSlashes(data.fileKeyStr) + '/' + pass; + } }; // V1 @@ -95,12 +101,22 @@ define([ }; Hash.createRandomHash = function (type, password) { - var cryptor = Crypto.createEditCryptor2(void 0, void 0, password); + var cryptor; + if (type === 'file') { + cryptor = Crypto.createFileCryptor2(void 0, password); + return getFileHashFromKeys({ + password: Boolean(password), + version: 2, + type: type, + keys: cryptor.fileKeyStr + }); + } + cryptor = Crypto.createEditCryptor2(void 0, void 0, password); return getEditHashFromKeys({ password: Boolean(password), version: 2, type: type, - keys: { editKeyStr: cryptor.editKeyStr } + keys: cryptor.editKeyStr }); }; @@ -113,6 +129,7 @@ Version 1 var parseTypeHash = Hash.parseTypeHash = function (type, hash) { if (!hash) { return; } + var options; var parsed = {}; var hashArr = fixDuplicateSlashes(hash).split('/'); if (['media', 'file', 'user', 'invite'].indexOf(type) === -1) { @@ -125,7 +142,6 @@ Version 1 parsed.version = 0; return parsed; } - var options; if (hashArr[1] && hashArr[1] === '1') { // Version 1 parsed.version = 1; parsed.mode = hashArr[2]; @@ -175,6 +191,25 @@ Version 1 parsed.key = hashArr[3].replace(/-/g, '/'); return parsed; } + if (hashArr[1] && hashArr[1] === '2') { // Version 2 + parsed.version = 2; + parsed.app = hashArr[2]; + parsed.key = hashArr[3]; + + options = hashArr.slice(4); + 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, 4).join('/') + '/'; + if (parsed.password) { hash += 'p/'; } + if (opts.embed) { hash += 'embed/'; } + if (opts.present) { hash += 'present/'; } + return hash; + }; + return parsed; + } return parsed; } if (['user'].indexOf(type) !== -1) { @@ -309,11 +344,12 @@ Version 1 } } } else if (parsed.type === "file") { - // version 2 hashes are to be used for encrypted blobs - secret.channel = parsed.channel; - secret.keys = { fileKeyStr: parsed.key }; + secret.channel = base64ToHex(parsed.channel); + secret.keys = { + fileKeyStr: parsed.key, + cryptKey: Nacl.util.decodeBase64(parsed.key) + }; } else if (parsed.type === "user") { - // 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) { @@ -338,7 +374,12 @@ Version 1 } } } else if (parsed.type === "file") { - throw new Error("File hashes should be version 1"); + secret.channel = base64ToHex(secret.keys.chanId); + secret.keys = Crypto.createFileCryptor2(parsed.key, password); + secret.key = secret.keys.fileKeyStr; + if (secret.channel.length !== 48 || secret.key.length !== 24) { + throw new Error("The channel key and/or the encryption key is invalid"); + } } else if (parsed.type === "user") { throw new Error("User hashes can't be opened (yet)"); } diff --git a/www/common/common-thumbnail.js b/www/common/common-thumbnail.js index ab6d3e6d4..bc8145d46 100644 --- a/www/common/common-thumbnail.js +++ b/www/common/common-thumbnail.js @@ -250,17 +250,15 @@ define([ var k = getKey(parsed.type, channel); common.setThumbnail(k, b64, cb); }; - Thumb.displayThumbnail = function (common, href, channel, $container, cb) { + Thumb.displayThumbnail = function (common, href, channel, password, $container, cb) { cb = cb || function () {}; var parsed = Hash.parsePadUrl(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 secret = Hash.getSecrets('file', parsed.hash, password); + var hexFileName = channel; var src = Hash.getBlobPathFromHex(hexFileName); - var cryptKey = secret.keys && secret.keys.fileKeyStr; - var key = Nacl.util.decodeBase64(cryptKey); + var key = secret.keys && secret.keys.cryptKey; FileCrypto.fetchDecryptedMetadata(src, key, function (e, metadata) { if (e) { if (e === 'XHR_ERROR') { return; } diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 3ba800b49..2169e1640 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1169,8 +1169,8 @@ define([ // No password for avatars var secret = Hash.getSecrets('file', parsed.hash); if (secret.keys && secret.channel) { - var cryptKey = secret.keys && secret.keys.fileKeyStr; - var hexFileName = Util.base64ToHex(secret.channel); + var hexFileName = secret.channel; + var cryptKey = Hash.encodeBase64(secret.keys && secret.keys.cryptKey); var src = Hash.getBlobPathFromHex(hexFileName); Common.getFileSize(hexFileName, function (e, data) { if (e || !data) { diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 5faffc5c9..68debf694 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -578,7 +578,6 @@ define([ } var parsed = Hash.parsePadUrl(window.location.href); if (!parsed.type || !parsed.hashData) { return void cb('E_INVALID_HREF'); } - if (parsed.type === 'file' && typeof(parsed.channel) === 'string') { secret.channel = Util.base64ToHex(secret.channel); } hashes = Hash.getHashes(secret); if (secret.version === 0) { diff --git a/www/common/diffMarked.js b/www/common/diffMarked.js index 80c9c4b79..fca3023b4 100644 --- a/www/common/diffMarked.js +++ b/www/common/diffMarked.js @@ -41,11 +41,15 @@ define([ }; renderer.image = function (href, title, text) { if (href.slice(0,6) === '/file/') { - // PASSWORD_FILES + // DEPRECATED + // Mediatag using markdown syntax should not be used anymore so they don't support + // password-protected files + console.log('DEPRECATED: mediatag using markdown syntax!'); var parsed = Hash.parsePadUrl(href); - var hexFileName = Util.base64ToHex(parsed.hashData.channel); - var src = '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName; - var mt = ''; + var secret = Hash.getSecrets('file', parsed.hash); + var src = Hash.getBlobPathFromHex(secret.channel); + var key = Hash.encodeBase64(secret.keys.cryptKey); + var mt = ''; if (mediaMap[src]) { mt += mediaMap[src]; } diff --git a/www/common/migrate-user-object.js b/www/common/migrate-user-object.js index 1d6dd1210..fb69fb20b 100644 --- a/www/common/migrate-user-object.js +++ b/www/common/migrate-user-object.js @@ -115,13 +115,8 @@ define([ parsed = Hash.parsePadUrl(el.href); if (!el.href) { return; } if (!el.channel) { - if (parsed.hashData && parsed.hashData.type === "file") { - // PASSWORD_FILES - el.channel = Util.base64ToHex(parsed.hashData.channel); - } else { - var secret = Hash.getSecrets(parsed.type, parsed.hash, el.password); - el.channel = secret.channel; - } + var secret = Hash.getSecrets(parsed.type, parsed.hash, el.password); + el.channel = secret.channel; progress(6, Math.round(100*i/padsLength)); console.log('Adding missing channel in filesData ', el.channel); } diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index e02a527ae..20cea6cd0 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -92,7 +92,7 @@ define([ var profileChan = profile.edit ? Hash.hrefToHexChannelId('/profile/#' + profile.edit, null) : null; if (profileChan) { list.push(profileChan); } var avatarChan = profile.avatar ? Hash.hrefToHexChannelId(profile.avatar, null) : null; - if (avatarChan) { list.push(Util.base64ToHex(avatarChan)); } + if (avatarChan) { list.push(avatarChan); } } if (store.proxy.friends) { diff --git a/www/common/outer/upload.js b/www/common/outer/upload.js index 7f3874511..9ccc32500 100644 --- a/www/common/outer/upload.js +++ b/www/common/outer/upload.js @@ -13,7 +13,16 @@ define([ // if it exists, path contains the new pad location in the drive var path = file.path; - var key = Nacl.randomBytes(32); + // XXX + // PASSWORD_FILES + var password; + var hash = Hash.createRandomHash('file', password); + var secret = Hash.getSecrets('file', hash, password); + var key = secret.keys.cryptKey; + var id = secret.channel; + //var key = Nacl.randomBytes(32); + + // XXX provide channel id to "next" var next = FileCrypto.encrypt(u8, metadata, key); var estimate = FileCrypto.computeEncryptedSize(u8.length, metadata); @@ -44,21 +53,11 @@ define([ } // if not box then done - common.uploadComplete(function (e, id) { + common.uploadComplete(function (e/*, id*/) { // XXX id is given, not asked if (e) { return void console.error(e); } var uri = ['', 'blob', id.slice(0,2), id].join('/'); console.log("encrypted blob is now available as %s", uri); - var b64Key = Nacl.util.encodeBase64(key); - - var secret = { - version: 1, - channel: id, - keys: { - fileKeyStr: b64Key - } - }; - var hash = Hash.getFileHashFromKeys(secret); var href = '/file/#' + hash; var title = metadata.name; diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js index b24092673..8b2270797 100644 --- a/www/common/outer/userObject.js +++ b/www/common/outer/userObject.js @@ -589,14 +589,9 @@ define([ // Fix channel if (!el.channel) { try { - if (parsed.hashData && parsed.hashData.type === "file") { - // PASSWORD_FILES - el.channel = Util.base64ToHex(parsed.hashData.channel); - } else { var secret = Hash.getSecrets(parsed.type, parsed.hash, el.password); el.channel = secret.channel; - } - console.log('Adding missing channel in filesData ', el.channel); + console.log('Adding missing channel in filesData ', el.channel); } catch (e) { console.error(e); } diff --git a/www/common/sframe-common-codemirror.js b/www/common/sframe-common-codemirror.js index 19b4c7ae0..0c1a6a7a5 100644 --- a/www/common/sframe-common-codemirror.js +++ b/www/common/sframe-common-codemirror.js @@ -329,15 +329,11 @@ define([ dropArea: $('.CodeMirror'), body: $('body'), onUploaded: function (ev, data) { - //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; - var mt = ''; + var secret = Hash.getSecrets('file', parsed.hash, data.password); + var src = Hash.getBlobPathFromHex(secret.channel); + var key = Hash.encodeBase64(secret.keys.cryptKey); + var mt = ''; editor.replaceSelection(mt); } }; diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 4fc5a6ac6..572fe5f4a 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -113,16 +113,16 @@ define([ return ''; }; funcs.getMediatagFromHref = function (href) { - // PASSWORD_FILES - var parsed = Hash.parsePadUrl(href); - var secret = Hash.getSecrets('file', parsed.hash); + // XXX: Should only be used with the current href var data = ctx.metadataMgr.getPrivateData(); + var parsed = Hash.parsePadUrl(href); + var secret = Hash.getSecrets('file', parsed.hash, data.password); if (secret.keys && secret.channel) { - var cryptKey = secret.keys && secret.keys.fileKeyStr; - var hexFileName = Util.base64ToHex(secret.channel); + var key = Hash.encodeBase64(secret.keys && secret.keys.cryptKey); + var hexFileName = secret.channel; var origin = data.fileHost || data.origin; var src = origin + Hash.getBlobPathFromHex(hexFileName); - return '' + + return '' + ''; } return; diff --git a/www/common/userObject.js b/www/common/userObject.js index b05798946..fb39eb484 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -590,7 +590,6 @@ define([ } }, cb); } - console.log(path, newName); if (path.length <= 1) { logError('Renaming `root` is forbidden'); return; diff --git a/www/drive/inner.js b/www/drive/inner.js index b668429cd..d072992d6 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -1305,7 +1305,7 @@ define([ $span.attr('title', name); var type = Messages.type[hrefData.type] || hrefData.type; - common.displayThumbnail(data.href, data.channel, $span, function ($thumb) { + common.displayThumbnail(data.href, data.channel, data.password, $span, function ($thumb) { // Called only if the thumbnail exists // Remove the .hide() added by displayThumnail() because it hides the icon in // list mode too diff --git a/www/file/inner.js b/www/file/inner.js index 3d752d95e..d472cb95d 100644 --- a/www/file/inner.js +++ b/www/file/inner.js @@ -54,17 +54,14 @@ define([ var uploadMode = false; var secret; - var hexFileName; var metadataMgr = common.getMetadataMgr(); var priv = metadataMgr.getPrivateData(); if (!priv.filehash) { uploadMode = true; } else { - // PASSWORD_FILES - secret = Hash.getSecrets('file', priv.filehash); + secret = Hash.getSecrets('file', priv.filehash, priv.password); if (!secret.keys) { throw new Error("You need a hash"); } - hexFileName = Util.base64ToHex(secret.channel); } var Title = common.createTitle({}); @@ -87,9 +84,10 @@ define([ toolbar.$rightside.html(''); if (!uploadMode) { + var hexFileName = secret.channel; var src = Hash.getBlobPathFromHex(hexFileName); - var cryptKey = secret.keys && secret.keys.fileKeyStr; - var key = Nacl.util.decodeBase64(cryptKey); + var key = secret.keys && secret.keys.cryptKey; + var cryptKey = Nacl.util.encodeBase64(key); FileCrypto.fetchDecryptedMetadata(src, key, function (e, metadata) { if (e) { @@ -118,9 +116,7 @@ define([ }; var $mt = $dlview.find('media-tag'); - var cryptKey = secret.keys && secret.keys.fileKeyStr; - var hexFileName = Util.base64ToHex(secret.channel); - $mt.attr('src', '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName); + $mt.attr('src', src); $mt.attr('data-crypto-key', 'cryptpad:'+cryptKey); var rightsideDisplayed = false; @@ -263,7 +259,7 @@ define([ dropArea: $form, hoverArea: $label, body: $body, - keepTable: true // Don't fadeOut the tbale with the uploaded files + keepTable: true // Don't fadeOut the table with the uploaded files }; var FM = common.createFileManager(fmConfig); diff --git a/www/filepicker/inner.js b/www/filepicker/inner.js index 9dd05076a..c30ada772 100644 --- a/www/filepicker/inner.js +++ b/www/filepicker/inner.js @@ -40,14 +40,14 @@ define([ var parsed = Hash.parsePadUrl(data.url); hideFileDialog(); if (parsed.type === 'file') { - // PASSWORD_FILES - var hexFileName = Util.base64ToHex(parsed.hashData.channel); - var src = '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName; + var secret = Hash.getSecrets('file', parsed.hash, data.password); + var src = Hash.getBlobPathFromHex(secret.channel); + var key = Hash.encodeBase64(secret.keys.cryptKey); sframeChan.event("EV_FILE_PICKED", { type: parsed.type, src: src, name: data.name, - key: parsed.hashData.key + key: key }); return; } @@ -69,8 +69,8 @@ define([ APP.FM = common.createFileManager(fmConfig); // Create file picker - var onSelect = function (url, name) { - onFilePicked({url: url, name: name}); + var onSelect = function (url, name, password) { + onFilePicked({url: url, name: name, password: password}); }; var data = { FM: APP.FM @@ -135,11 +135,13 @@ define([ $('', {'class': 'cp-filepicker-content-element-name'}).text(name) .appendTo($span); $span.click(function () { - if (typeof onSelect === "function") { onSelect(data.href, name); } + if (typeof onSelect === "function") { + onSelect(data.href, name, data.password); + } }); // Add thumbnail if it exists - common.displayThumbnail(data.href, data.channel, $span); + common.displayThumbnail(data.href, data.channel, data.password, $span); }); $input.focus(); }; diff --git a/www/pad/inner.js b/www/pad/inner.js index 5b150c419..b8199bc61 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -552,11 +552,11 @@ define([ ckeditor: editor, body: $('body'), onUploaded: function (ev, data) { - // PASSWORD_FILES var parsed = Hash.parsePadUrl(data.url); - var hexFileName = Util.base64ToHex(parsed.hashData.channel); - var src = '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName; - var mt = ''; + var secret = Hash.getSecrets('file', parsed.hash, data.password); + var src = Hash.getBlobPathFromHex(secret.channel); + var key = Hash.encodeBase64(secret.keys.cryptKey); + var mt = ''; // MEDIATAG var element = window.CKEDITOR.dom.element.createFromHtml(mt); editor.insertElement(element); diff --git a/www/slide/inner.js b/www/slide/inner.js index 18b7da4e3..497b0b8ca 100644 --- a/www/slide/inner.js +++ b/www/slide/inner.js @@ -500,11 +500,11 @@ define([ dropArea: $('.CodeMirror'), body: $('body'), onUploaded: function (ev, data) { - // PASSWORD_FILES var parsed = Hash.parsePadUrl(data.url); - var hexFileName = Util.base64ToHex(parsed.hashData.channel); - var src = '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName; - var mt = ''; + var secret = Hash.getSecrets('file', parsed.hash, data.password); + var src = Hash.getBlobPathFromHex(secret.channel); + var key = Hash.encodeBase64(secret.keys.cryptKey); + var mt = ''; editor.replaceSelection(mt); } };