Merge branch 'donkey' into staging

pull/1/head
yflory 7 years ago
commit 21366c252d

@ -203,6 +203,10 @@
padding: @alertify_padding-base;
}
span.cp-password-container {
margin-bottom: 15px;
}
input[type="checkbox"], input[type="radio"] {
width: auto;
padding: 0;

@ -5,6 +5,7 @@
input {
flex: 1;
min-width: 0;
margin-bottom: 0 !important; // Override margin from alertify
}
label, .fa {
margin-left: 10px;

@ -597,9 +597,9 @@ define(function () {
out.settings_templateSkipHint = "Quand vous créez un nouveau pad, et si vous possédez des modèles pour ce type de pad, une fenêtre peut apparaître pour demander si vous souhaitez importer un modèle. Ici vous pouvez choisir de ne jamais montrer cette fenêtre et donc de ne jamais utiliser de modèle.";
out.upload_title = "Hébergement de fichiers";
out.upload_rename = "Souhaitez-vous renommer <b>{0}</b> avant son stockage en ligne ?<br>" +
"<em>L'extension du fichier ({1}) sera ajoutée automatiquement. "+
"Ce nom sera permanent et visible par les autres utilisateurs</em>.";
out.upload_modal_title = "Options d'importation du fichier";
out.upload_modal_filename = "Nom (extension <em>{0}</em> ajoutée automatiquement)";
out.upload_modal_owner = "Être propriétaire du fichier";
out.upload_serverError = "Erreur interne: impossible d'importer le fichier pour l'instant.";
out.upload_uploadPending = "Vous avez déjà un fichier en cours d'importation. Souhaitez-vous l'annuler et importer ce nouveau fichier ?";
out.upload_success = "Votre fichier ({0}) a été importé avec succès et ajouté à votre CryptDrive.";

@ -601,9 +601,9 @@ define(function () {
out.settings_templateSkipHint = "When you create a new empty pad, if you have stored templates for this type of pad, a modal appears to ask if you want to use a template. Here you can choose to never show this modal and so to never use a template.";
out.upload_title = "File upload";
out.upload_rename = "Do you want to rename <b>{0}</b> before uploading it to the server?<br>" +
"<em>The file extension ({1}) will be added automatically. "+
"This name will be permanent and visible to other users.</em>";
out.upload_modal_title = "File upload options";
out.upload_modal_filename = "File name (extension <em>{0}</em> added automatically)";
out.upload_modal_owner = "Owned file";
out.upload_serverError = "Server Error: unable to upload your file at this time.";
out.upload_uploadPending = "You already have an upload in progress. Cancel it and upload your new file?";
out.upload_success = "Your file ({0}) has been successfully uploaded and added to your drive.";

261
rpc.js

@ -36,6 +36,7 @@ var isValidId = function (chan) {
[32, 48].indexOf(chan.length) > -1;
};
/*
var uint8ArrayToHex = function (a) {
// call slice so Uint8Arrays work as expected
return Array.prototype.slice.call(a).map(function (e) {
@ -52,14 +53,24 @@ var uint8ArrayToHex = function (a) {
}
}).join('');
};
*/
var testFileId = function (id) {
if (id.length !== 48 || /[^a-f0-9]/.test(id)) {
return false;
}
return true;
};
/*
var createFileId = function () {
var id = uint8ArrayToHex(Nacl.randomBytes(24));
if (id.length !== 48 || /[^a-f0-9]/.test(id)) {
if (!testFileId(id)) {
throw new Error('file ids must consist of 48 hex characters');
}
return id;
};
*/
var makeToken = function () {
return Number(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER))
@ -811,6 +822,17 @@ var makeFileStream = function (root, id, cb) {
});
};
var isFile = function (filePath, cb) {
/*:: if (typeof(filePath) !== 'string') { throw new Error('should never happen'); } */
Fs.stat(filePath, function (e, stats) {
if (e) {
if (e.code === 'ENOENT') { return void cb(void 0, false); }
return void cb(e.message);
}
return void cb(void 0, stats.isFile());
});
};
var clearOwnedChannel = function (Env, channelId, unsafeKey, cb) {
if (typeof(channelId) !== 'string' || channelId.length !== 32) {
return cb('INVALID_ARGUMENTS');
@ -834,11 +856,66 @@ var clearOwnedChannel = function (Env, channelId, unsafeKey, cb) {
});
};
var removeOwnedBlob = function (Env, blobId, unsafeKey, cb) {
var safeKey = escapeKeyCharacters(unsafeKey);
var safeKeyPrefix = safeKey.slice(0,3);
var blobPrefix = blobId.slice(0,2);
var blobPath = makeFilePath(Env.paths.blob, blobId);
var ownPath = Path.join(Env.paths.blob, safeKeyPrefix, safeKey, blobPrefix, blobId);
nThen(function (w) {
// Check if the blob exists
isFile(blobPath, w(function (e, isFile) {
if (e) {
w.abort();
return void cb(e);
}
if (!isFile) {
WARN('removeOwnedBlob', 'The provided blob ID is not a file!');
w.abort();
return void cb('EINVAL_BLOBID');
}
}));
}).nThen(function (w) {
// Check if you're the owner
isFile(ownPath, w(function (e, isFile) {
if (e) {
w.abort();
return void cb(e);
}
if (!isFile) {
WARN('removeOwnedBlob', 'Incorrect owner');
w.abort();
return void cb('INSUFFICIENT_PERMISSIONS');
}
}));
}).nThen(function (w) {
// Delete the blob
/*:: if (typeof(blobPath) !== 'string') { throw new Error('should never happen'); } */
Fs.unlink(blobPath, w(function (e) {
if (e) {
w.abort();
return void cb(e);
}
}));
}).nThen(function () {
// Delete the proof of ownership
Fs.unlink(ownPath, function (e) {
cb(e);
});
});
};
var removeOwnedChannel = function (Env, channelId, unsafeKey, cb) {
if (typeof(channelId) !== 'string' || channelId.length !== 32) {
if (typeof(channelId) !== 'string' || !isValidId(channelId)) {
return cb('INVALID_ARGUMENTS');
}
if (testFileId(channelId)) {
return void removeOwnedBlob(Env, channelId, unsafeKey, cb);
}
if (!(Env.msgStore && Env.msgStore.removeChannel && Env.msgStore.getChannelMetadata)) {
return cb("E_NOT_IMPLEMENTED");
}
@ -876,7 +953,7 @@ var upload = function (Env, publicKey, content, cb) {
var session = getSession(Env.Sessions, publicKey);
if (typeof(session.currentUploadSize) !== 'number' ||
typeof(session.currentUploadSize) !== 'number') {
typeof(session.pendingUploadSize) !== 'number') {
// improperly initialized... maybe they didn't check before uploading?
// reject it, just in case
return cb('NOT_READY');
@ -902,12 +979,12 @@ var upload = function (Env, publicKey, content, cb) {
}
};
var upload_cancel = function (Env, publicKey, cb) {
var upload_cancel = function (Env, publicKey, fileSize, cb) {
var paths = Env.paths;
var session = getSession(Env.Sessions, publicKey);
delete session.currentUploadSize;
delete session.pendingUploadSize;
session.pendingUploadSize = fileSize;
session.currentUploadSize = 0;
if (session.blobstage) { session.blobstage.close(); }
var path = makeFilePath(paths.staging, publicKey);
@ -923,17 +1000,7 @@ var upload_cancel = function (Env, publicKey, cb) {
});
};
var isFile = function (filePath, cb) {
Fs.stat(filePath, function (e, stats) {
if (e) {
if (e.code === 'ENOENT') { return void cb(void 0, false); }
return void cb(e.message);
}
return void cb(void 0, stats.isFile());
});
};
var upload_complete = function (Env, publicKey, cb) {
var upload_complete = function (Env, publicKey, id, cb) {
var paths = Env.paths;
var session = getSession(Env.Sessions, publicKey);
@ -942,14 +1009,18 @@ var upload_complete = function (Env, publicKey, cb) {
delete session.blobstage;
}
if (!testFileId(id)) {
WARN('uploadComplete', "id is invalid");
return void cb('EINVAL_ID');
}
var oldPath = makeFilePath(paths.staging, publicKey);
if (!oldPath) {
WARN('safeMkdir', "oldPath is null");
return void cb('RENAME_ERR');
}
var tryRandomLocation = function (cb) {
var id = createFileId();
var tryLocation = function (cb) {
var prefix = id.slice(0, 2);
var newPath = makeFilePath(paths.blob, id);
if (typeof(newPath) !== 'string') {
@ -968,7 +1039,8 @@ var upload_complete = function (Env, publicKey, cb) {
return void cb(e);
}
if (yes) {
return void tryRandomLocation(cb);
WARN('isFile', 'FILE EXISTS!');
return void cb('RENAME_ERR');
}
cb(void 0, newPath, id);
@ -976,40 +1048,25 @@ var upload_complete = function (Env, publicKey, cb) {
});
};
var retries = 3;
var handleMove = function (e, newPath, id) {
if (e || !oldPath || !newPath) {
if (retries--) {
setTimeout(function () {
return tryRandomLocation(handleMove);
}, 750);
} else {
cb(e);
}
return;
return void cb(e || 'PATH_ERR');
}
// lol wut handle ur errors
Fs.rename(oldPath, newPath, function (e) {
if (e) {
WARN('rename', e);
if (retries--) {
return void setTimeout(function () {
tryRandomLocation(handleMove);
}, 750);
}
return void cb('RENAME_ERR');
}
cb(void 0, id);
});
};
tryRandomLocation(handleMove);
tryLocation(handleMove);
};
/*
var owned_upload_complete = function (Env, safeKey, cb) {
var session = getSession(Env.Sessions, safeKey);
@ -1069,7 +1126,7 @@ var owned_upload_complete = function (Env, safeKey, cb) {
var finalPath;
nThen(function (w) {
// make the requisite directory structure using Mkdirp
Mkdirp(plannedPath, w(function (e /*, path */) {
Mkdirp(plannedPath, w(function (e) {
if (e) { // does not throw error if the directory already existed
w.abort();
return void cb(e);
@ -1087,10 +1144,123 @@ var owned_upload_complete = function (Env, safeKey, cb) {
}).nThen(function (w) {
// move the existing file to its new path
// flow is dumb and I need to guard against this which will never happen
// / *:: if (typeof(oldPath) === 'object') { throw new Error('should never happen'); } * /
Fs.rename(oldPath, finalPath, w(function (e) {
if (e) {
w.abort();
return void cb(e.code);
}
// otherwise it worked...
}));
}).nThen(function () {
// clean up their session when you're done
// call back with the blob id...
cb(void 0, blobId);
});
};
*/
var owned_upload_complete = function (Env, safeKey, id, cb) {
var session = getSession(Env.Sessions, safeKey);
// the file has already been uploaded to the staging area
// close the pending writestream
if (session.blobstage && session.blobstage.close) {
session.blobstage.close();
delete session.blobstage;
}
if (!testFileId(id)) {
WARN('ownedUploadComplete', "id is invalid");
return void cb('EINVAL_ID');
}
var oldPath = makeFilePath(Env.paths.staging, safeKey);
if (typeof(oldPath) !== 'string') {
return void cb('EINVAL_CONFIG');
}
// construct relevant paths
var root = Env.paths.blob;
//var safeKey = escapeKeyCharacters(safeKey);
var safeKeyPrefix = safeKey.slice(0, 3);
//var blobId = createFileId();
var blobIdPrefix = id.slice(0, 2);
var ownPath = Path.join(root, safeKeyPrefix, safeKey, blobIdPrefix);
var filePath = Path.join(root, blobIdPrefix);
var tryId = function (path, cb) {
Fs.access(path, Fs.constants.R_OK | Fs.constants.W_OK, function (e) {
if (!e) {
// generate a new id (with the same prefix) and recurse
WARN('ownedUploadComplete', 'id is already used '+ id);
return void cb('EEXISTS');
} else if (e.code === 'ENOENT') {
// no entry, so it's safe for us to proceed
return void cb();
} else {
// it failed in an unexpected way. log it
WARN(e, 'ownedUploadComplete');
return void cb(e.code);
}
});
};
// the user wants to move it into blob and create a empty file with the same id
// in their own space:
// /blob/safeKeyPrefix/safeKey/blobPrefix/blobID
var finalPath;
var finalOwnPath;
nThen(function (w) {
// make the requisite directory structure using Mkdirp
Mkdirp(filePath, w(function (e /*, path */) {
if (e) { // does not throw error if the directory already existed
w.abort();
return void cb(e);
}
}));
Mkdirp(ownPath, w(function (e /*, path */) {
if (e) { // does not throw error if the directory already existed
w.abort();
return void cb(e);
}
}));
}).nThen(function (w) {
// make sure the id does not collide with another
finalPath = Path.join(filePath, id);
finalOwnPath = Path.join(ownPath, id);
tryId(finalPath, w(function (e) {
if (e) {
w.abort();
return void cb(e);
}
}));
}).nThen(function (w) {
// Create the empty file proving ownership
Fs.writeFile(finalOwnPath, '', w(function (e) {
if (e) {
w.abort();
return void cb(e.code);
}
// otherwise it worked...
}));
}).nThen(function (w) {
// move the existing file to its new path
// flow is dumb and I need to guard against this which will never happen
/*:: if (typeof(oldPath) === 'object') { throw new Error('should never happen'); } */
Fs.rename(oldPath /* XXX */, finalPath, w(function (e) {
if (e) {
// Remove the ownership file
// XXX not needed if we have a cleanup script?
Fs.unlink(finalOwnPath, function (e) {
WARN(e, 'Removing ownership file ownedUploadComplete');
});
w.abort();
return void cb(e.code);
}
@ -1099,7 +1269,7 @@ var owned_upload_complete = function (Env, safeKey, cb) {
}).nThen(function () {
// clean up their session when you're done
// call back with the blob id...
cb(void 0, blobId);
cb(void 0, id);
});
};
@ -1504,20 +1674,23 @@ RPC.create = function (
});
case 'UPLOAD_COMPLETE':
if (!privileged) { return deny(); }
return void upload_complete(Env, safeKey, function (e, hash) {
return void upload_complete(Env, safeKey, msg[1], function (e, hash) {
WARN(e, hash);
Respond(e, hash);
});
case 'OWNED_UPLOAD_COMPLETE':
if (!privileged) { return deny(); }
return void owned_upload_complete(Env, safeKey, function (e, blobId) {
return void owned_upload_complete(Env, safeKey, msg[1], function (e, blobId) {
WARN(e, blobId);
Respond(e, blobId);
});
case 'UPLOAD_CANCEL':
if (!privileged) { return deny(); }
return void upload_cancel(Env, safeKey, function (e) {
WARN(e);
// msg[1] is fileSize
// if we pass it here, we can start an upload right away without calling
// UPLOAD_STATUS again
return void upload_cancel(Env, safeKey, msg[1], function (e) {
WARN(e, 'UPLOAD_CANCEL');
Respond(e);
});
default:

@ -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 = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + parsed.hashData.key + '"></media-tag>';
var secret = Hash.getSecrets('file', parsed.hash, data.password);
var src = Hash.getBlobPathFromHex(secret.channel);
var key = Hash.encodeBase64(secret.keys.cryptKey);
var mt = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + key + '"></media-tag>';
editor.replaceSelection(mt);
}
};

@ -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,25 +60,13 @@ define([
return '/1/' + hexToBase64(secret.channel) + '/' +
Crypto.b64RemoveSlashes(data.fileKeyStr) + '/';
}
};
// V1
/*var getEditHashFromKeys = Hash.getEditHashFromKeys = function (chanKey, keys) {
if (typeof keys === 'string') {
return chanKey + keys;
}
if (!keys.editKeyStr) { return; }
return '/1/edit/' + hexToBase64(chanKey) + '/'+Crypto.b64RemoveSlashes(keys.editKeyStr)+'/';
};
var getViewHashFromKeys = Hash.getViewHashFromKeys = function (chanKey, keys) {
if (typeof keys === 'string') {
return;
if (version === 2) {
if (!data.fileKeyStr) { return; }
var pass = secret.password ? 'p/' : '';
return '/2/' + secret.type + '/' + Crypto.b64RemoveSlashes(data.fileKeyStr) + '/' + pass;
}
return '/1/view/' + hexToBase64(chanKey) + '/'+Crypto.b64RemoveSlashes(keys.viewKeyStr)+'/';
};
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, '-');
};
@ -95,12 +84,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
});
}
cryptor = Crypto.createEditCryptor2(void 0, void 0, password);
return getEditHashFromKeys({
password: Boolean(password),
version: 2,
type: type,
keys: { editKeyStr: cryptor.editKeyStr }
keys: cryptor
});
};
@ -113,6 +112,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 +125,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 +174,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 +327,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 +357,12 @@ Version 1
}
}
} else if (parsed.type === "file") {
throw new Error("File hashes should be version 1");
secret.keys = Crypto.createFileCryptor2(parsed.key, password);
secret.channel = base64ToHex(secret.keys.chanId);
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)");
}

@ -425,7 +425,8 @@ define([
cb = cb || function () {};
opt = opt || {};
var input = dialog.textInput();
var inputBlock = opt.password ? UI.passwordInput() : dialog.textInput();
var input = opt.password ? $(inputBlock).find('input')[0] : inputBlock;
input.value = typeof(def) === 'string'? def: '';
var message;
@ -441,7 +442,7 @@ define([
var cancel = dialog.cancelButton(opt.cancel);
var frame = dialog.frame([
message,
input,
inputBlock,
dialog.nav([ cancel, ok, ]),
]);

@ -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; }

@ -162,7 +162,6 @@ define([
$pwInput.val(data.password).click(function () {
$pwInput[0].select();
});
$(password).find('.cp-checkmark').css('margin-bottom', '15px');
$d.append(password);
}
@ -960,7 +959,7 @@ define([
};
};
var setHTML = function (e, html) {
var setHTML = UIElements.setHTML = function (e, html) {
e.innerHTML = html;
return e;
};
@ -1172,8 +1171,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) {

@ -204,8 +204,8 @@ define([
});
};
common.uploadComplete = function (cb) {
postMessage("UPLOAD_COMPLETE", null, function (obj) {
common.uploadComplete = function (id, owned, cb) {
postMessage("UPLOAD_COMPLETE", {id: id, owned: owned}, function (obj) {
if (obj && obj.error) { return void cb(obj.error); }
cb(null, obj);
});
@ -218,8 +218,8 @@ define([
});
};
common.uploadCancel = function (cb) {
postMessage("UPLOAD_CANCEL", null, function (obj) {
common.uploadCancel = function (size, cb) {
postMessage("UPLOAD_CANCEL", {size: size}, function (obj) {
if (obj && obj.error) { return void cb(obj.error); }
cb(null, obj);
});
@ -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) {

@ -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 = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + parsed.hashData.key + '">';
var secret = Hash.getSecrets('file', parsed.hash);
var src = Hash.getBlobPathFromHex(secret.channel);
var key = Hash.encodeBase64(secret.keys.cryptKey);
var mt = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + key + '"></media-tag>';
if (mediaMap[src]) {
mt += mediaMap[src];
}

@ -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);
}

@ -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) {
@ -232,7 +232,16 @@ define([
Store.uploadComplete = function (data, cb) {
if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
store.rpc.uploadComplete(function (err, res) {
if (data.owned) {
// Owned file
store.rpc.ownedUploadComplete(data.id, function (err, res) {
if (err) { return void cb({error:err}); }
cb(res);
});
return;
}
// Normal upload
store.rpc.uploadComplete(data.id, function (err, res) {
if (err) { return void cb({error:err}); }
cb(res);
});
@ -248,7 +257,7 @@ define([
Store.uploadCancel = function (data, cb) {
if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
store.rpc.uploadCancel(function (err, res) {
store.rpc.uploadCancel(data.size, function (err, res) {
if (err) { return void cb({error:err}); }
cb(res);
});
@ -678,6 +687,7 @@ define([
if (Store.channel && Store.channel.wc && channel === Store.channel.wc.id) {
owners = Store.channel.data.owners || undefined;
}
var expire;
if (Store.channel && Store.channel.wc && channel === Store.channel.wc.id) {
expire = +Store.channel.data.expire || undefined;
@ -726,7 +736,11 @@ define([
contains = true;
pad.atime = +new Date();
pad.title = title;
pad.owners = owners;
if (owners || h.type !== "file") {
// OWNED_FILES
// Never remove owner for files
pad.owners = owners;
}
pad.expire = expire;
// If the href is different, it means we have a stronger one

@ -1,8 +1,9 @@
define([
'/file/file-crypto.js',
'/common/common-hash.js',
'/bower_components/nthen/index.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
], function (FileCrypto, Hash) {
], function (FileCrypto, Hash, nThen) {
var Nacl = window.nacl;
var module = {};
@ -10,97 +11,122 @@ define([
var u8 = file.blob; // This is not a blob but a uint8array
var metadata = file.metadata;
var owned = file.isOwned;
// XXX
owned = true;
// if it exists, path contains the new pad location in the drive
var path = file.path;
var key = Nacl.randomBytes(32);
var next = FileCrypto.encrypt(u8, metadata, key);
var estimate = FileCrypto.computeEncryptedSize(u8.length, metadata);
var password = file.password;
var hash, secret, key, id, href;
var sendChunk = function (box, cb) {
var enc = Nacl.util.encodeBase64(box);
common.uploadChunk(enc, function (e, msg) {
cb(e, msg);
});
var getNewHash = function () {
hash = Hash.createRandomHash('file', password);
secret = Hash.getSecrets('file', hash, password);
key = secret.keys.cryptKey;
id = secret.channel;
href = '/file/#' + hash;
};
var actual = 0;
var again = function (err, box) {
if (err) { throw new Error(err); }
if (box) {
actual += box.length;
var progressValue = (actual / estimate * 100);
updateProgress(progressValue);
return void sendChunk(box, function (e) {
if (e) { return console.error(e); }
next(again);
});
}
if (actual !== estimate) {
console.error('Estimated size does not match actual size');
}
// if not box then done
common.uploadComplete(function (e, id) {
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;
if (noStore) { return void onComplete(href); }
// PASSWORD_FILES
var data = {
title: title || "",
href: href,
path: path,
channel: id
};
common.setPadTitle(data, function (err) {
if (err) { return void console.error(err); }
onComplete(href);
common.setPadAttribute('fileType', metadata.type, null, href);
});
var getValidHash = function (cb) {
getNewHash();
common.getFileSize(href, password, function (err, size) {
if (err || typeof(size) !== "number") { throw new Error(err || "Invalid size!"); }
if (size === 0) { return void cb(); }
getValidHash();
});
};
common.uploadStatus(estimate, function (e, pending) {
if (e) {
console.error(e);
onError(e);
return;
}
if (pending) {
return void onPending(function () {
// if the user wants to cancel the pending upload to execute that one
common.uploadCancel(function (e, res) {
if (e) {
return void console.error(e);
}
console.log(res);
var edPublic;
nThen(function (waitFor) {
// Generate a hash and check if the resulting id is valid (not already used)
getValidHash(waitFor());
}).nThen(function (waitFor) {
if (!owned) { return; }
common.getMetadata(waitFor(function (err, m) {
edPublic = m.priv.edPublic;
metadata.owners = [edPublic];
}));
}).nThen(function () {
var next = FileCrypto.encrypt(u8, metadata, key);
var estimate = FileCrypto.computeEncryptedSize(u8.length, metadata);
var sendChunk = function (box, cb) {
var enc = Nacl.util.encodeBase64(box);
common.uploadChunk(enc, function (e, msg) {
cb(e, msg);
});
};
var actual = 0;
var again = function (err, box) {
if (err) { throw new Error(err); }
if (box) {
actual += box.length;
var progressValue = (actual / estimate * 100);
updateProgress(progressValue);
return void sendChunk(box, function (e) {
if (e) { return console.error(e); }
next(again);
});
}
if (actual !== estimate) {
console.error('Estimated size does not match actual size');
}
// if not box then done
common.uploadComplete(id, owned, function (e) {
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 title = metadata.name;
if (noStore) { return void onComplete(href); }
var data = {
title: title || "",
href: href,
path: path,
password: password,
channel: id
};
common.setPadTitle(data, function (err) {
if (err) { return void console.error(err); }
onComplete(href);
common.setPadAttribute('fileType', metadata.type, null, href);
common.setPadAttribute('owners', metadata.owners, null, href);
});
});
}
next(again);
};
common.uploadStatus(estimate, function (e, pending) {
if (e) {
console.error(e);
onError(e);
return;
}
if (pending) {
return void onPending(function () {
// if the user wants to cancel the pending upload to execute that one
common.uploadCancel(estimate, function (e) {
if (e) {
return void console.error(e);
}
next(again);
});
});
}
next(again);
});
});
};
return module;
});

@ -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);
}

@ -151,7 +151,7 @@ define([
};
exp.removeOwnedChannel = function (channel, cb) {
if (typeof(channel) !== 'string' || channel.length !== 32) {
if (typeof(channel) !== 'string' || [32,48].indexOf(channel.length) === -1) {
// can't use this on files because files can't be owned...
return void cb('INVALID_ARGUMENTS');
}
@ -176,8 +176,19 @@ define([
});
};
exp.uploadComplete = function (cb) {
rpc.send('UPLOAD_COMPLETE', null, function (e, res) {
exp.uploadComplete = function (id, cb) {
rpc.send('UPLOAD_COMPLETE', id, function (e, res) {
if (e) { return void cb(e); }
var id = res[0];
if (typeof(id) !== 'string') {
return void cb('INVALID_ID');
}
cb(void 0, id);
});
};
exp.ownedUploadComplete = function (id, cb) {
rpc.send('OWNED_UPLOAD_COMPLETE', id, function (e, res) {
if (e) { return void cb(e); }
var id = res[0];
if (typeof(id) !== 'string') {
@ -203,8 +214,8 @@ define([
});
};
exp.uploadCancel = function (cb) {
rpc.send('UPLOAD_CANCEL', void 0, function (e) {
exp.uploadCancel = function (size, cb) {
rpc.send('UPLOAD_CANCEL', size, function (e) {
if (e) { return void cb(e); }
cb();
});

@ -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 = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' +
parsed.hashData.key + '"></media-tag>';
var secret = Hash.getSecrets('file', parsed.hash, data.password);
var src = Hash.getBlobPathFromHex(secret.channel);
var key = Hash.encodeBase64(secret.keys.cryptKey);
var mt = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + key + '"></media-tag>';
editor.replaceSelection(mt);
}
};

@ -3,11 +3,13 @@ define([
'/file/file-crypto.js',
'/common/common-thumbnail.js',
'/common/common-interface.js',
'/common/common-ui-elements.js',
'/common/common-util.js',
'/common/hyperscript.js',
'/customize/messages.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
], function ($, FileCrypto, Thumb, UI, Util, Messages) {
], function ($, FileCrypto, Thumb, UI, UIElements, Util, h, Messages) {
var Nacl = window.nacl;
var module = {};
@ -26,6 +28,7 @@ define([
module.create = function (common, config) {
var File = {};
var origin = common.getMetadataMgr().getPrivateData().origin;
var queue = File.queue = {
queue: [],
@ -53,6 +56,7 @@ define([
data.name = file.metadata.name;
data.url = href;
data.password = file.password;
if (file.metadata.type.slice(0,6) === 'image/') {
data.mediatag = true;
}
@ -212,29 +216,61 @@ define([
queue.next();
};
// Don't show the rename prompt if we don't want to store the file in the drive (avatar)
var showNamePrompt = !config.noStore;
var promptName = function (file, cb) {
// Get the upload options
var fileUploadModal = function (file, cb) {
var extIdx = file.name.lastIndexOf('.');
var name = extIdx !== -1 ? file.name.slice(0,extIdx) : file.name;
var ext = extIdx !== -1 ? file.name.slice(extIdx) : "";
var msg = Messages._getKey('upload_rename', [
Util.fixHTML(file.name),
Util.fixHTML(ext)
var createHelper = function (href, text) {
var q = h('a.fa.fa-question-circle', {
style: 'text-decoration: none !important;',
title: text,
href: origin + href,
target: "_blank",
'data-tippy-placement': "right"
});
return q;
};
// Ask for name, password and owner
var content = h('div', [
h('h4', Messages.upload_modal_title),
UIElements.setHTML(h('label', {for: 'cp-upload-name'}),
Messages._getKey('upload_modal_filename', [ext])),
h('input#cp-upload-name', {type: 'text', placeholder: name}),
h('label', {for: 'cp-upload-password'}, Messages.creation_passwordValue),
UI.passwordInput({id: 'cp-upload-password'}),
h('span', {
style: 'display:flex;align-items:center;justify-content:space-between'
}, [
UI.createCheckbox('cp-upload-owned', Messages.upload_modal_owner, true),
createHelper('/faq.html#keywords-owned', Messages.creation_owned1)
]),
]);
UI.prompt(msg, name, function (newName) {
if (newName === null) {
showNamePrompt = false;
return void cb (file.name);
}
if (!newName || !newName.trim()) { return void cb (file.name); }
UI.confirm(content, function (yes) {
if (!yes) { return void cb(); }
// Get the values
var newName = $(content).find('#cp-upload-name').val();
var password = $(content).find('#cp-upload-password').val() || undefined;
var owned = $(content).find('#cp-upload-owned').is(':checked');
// Add extension to the name if needed
if (!newName || !newName.trim()) { newName = file.name; }
var newExtIdx = newName.lastIndexOf('.');
var newExt = newExtIdx !== -1 ? newName.slice(newExtIdx) : "";
if (newExt !== ext) { newName += ext; }
cb(newName);
}, {cancel: Messages.doNotAskAgain}, true);
cb({
name: newName,
password: password,
owned: owned
});
});
};
var handleFileState = {
queue: [],
inProgress: false
@ -246,17 +282,23 @@ define([
var thumb;
var file_arraybuffer;
var name = file.name;
var finish = function () {
var metadata = {
name: name,
type: file.type,
};
if (thumb) { metadata.thumbnail = thumb; }
queue.push({
blob: file_arraybuffer,
metadata: metadata,
dropEvent: e
});
var password;
var owned = true;
var finish = function (abort) {
if (!abort) {
var metadata = {
name: name,
type: file.type,
};
if (thumb) { metadata.thumbnail = thumb; }
queue.push({
blob: file_arraybuffer,
metadata: metadata,
password: password,
owned: owned,
dropEvent: e
});
}
handleFileState.inProgress = false;
if (handleFileState.queue.length) {
var next = handleFileState.queue.shift();
@ -264,9 +306,16 @@ define([
}
};
var getName = function () {
if (!showNamePrompt) { return void finish(); }
promptName(file, function (newName) {
name = newName;
// If "noStore", it means we don't want to store this file in our drive (avatar)
// In this case, we don't want a password or a filename, and we own the file
if (config.noStore) { return void finish(); }
// Otherwise, ask for password, name and ownership
fileUploadModal(file, function (obj) {
if (!obj) { return void finish(true); }
name = obj.name;
password = obj.password;
owned = obj.owned;
finish();
});
};

@ -150,12 +150,12 @@ define([
todo();
} else {
// Ask for the password and check if the pad exists
// If the pad doesn't exist, it means the password is oncorrect
// If the pad doesn't exist, it means the password isn't correct
// or the pad has been deleted
var correctPassword = waitFor();
sframeChan.on('Q_PAD_PASSWORD_VALUE', function (data, cb) {
password = data;
Cryptpad.isNewChannel(window.location.href, password, function (e, isNew) {
var next = function (e, isNew) {
if (Boolean(isNew)) {
// Ask again in the inner iframe
// We should receive a new Q_PAD_PASSWORD_VALUE
@ -165,7 +165,17 @@ define([
correctPassword();
cb(true);
}
});
};
if (parsed.type === "file") {
// `isNewChannel` doesn't work for files (not a channel)
// `getFileSize` is not adapted to channels because of metadata
Cryptpad.getFileSize(window.location.href, password, function (e, size) {
next(e, size === 0);
});
return;
}
// Not a file, so we can use `isNewChannel`
Cryptpad.isNewChannel(window.location.href, password, next);
});
sframeChan.event("EV_PAD_PASSWORD");
}

@ -113,16 +113,16 @@ define([
return '<script src="' + origin + '/common/media-tag-nacl.min.js"></script>';
};
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 '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + cryptKey + '">' +
return '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + key + '">' +
'</media-tag>';
}
return;

@ -590,7 +590,6 @@ define([
}
}, cb);
}
console.log(path, newName);
if (path.length <= 1) {
logError('Renaming `root` is forbidden');
return;

@ -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

@ -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) {
@ -98,17 +96,27 @@ define([
}
return void console.error(e);
}
// Add pad attributes when the file is saved in the drive
Title.onTitleChange(function () {
var owners = metadata.owners;
if (owners) {
common.setPadAttribute('owners', owners);
}
common.setPadAttribute('fileType', metadata.type);
});
// Save to the drive or update the acces time
var title = document.title = metadata.name;
Title.updateTitle(title || Title.defaultTitle);
toolbar.addElement(['pageTitle'], {pageTitle: title});
toolbar.$rightside.append(common.createButton('forget', true));
toolbar.$rightside.append(common.createButton('properties', true));
if (common.isLoggedIn()) {
toolbar.$rightside.append(common.createButton('hashtag', true));
}
common.setPadAttribute('fileType', metadata.type);
var displayFile = function (ev, sizeMb, CB) {
var called_back;
var cb = function (e) {
@ -118,9 +126,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 +269,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);

@ -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([
$('<span>', {'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();
};

@ -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 = '<media-tag contenteditable="false" src="' + src + '" data-crypto-key="cryptpad:' + parsed.hashData.key + '" tabindex="1"></media-tag>';
var secret = Hash.getSecrets('file', parsed.hash, data.password);
var src = Hash.getBlobPathFromHex(secret.channel);
var key = Hash.encodeBase64(secret.keys.cryptKey);
var mt = '<media-tag contenteditable="false" src="' + src + '" data-crypto-key="cryptpad:' + key + '"></media-tag>';
// MEDIATAG
var element = window.CKEDITOR.dom.element.createFromHtml(mt);
editor.insertElement(element);

@ -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 = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + parsed.hashData.key + '"></media-tag>';
var secret = Hash.getSecrets('file', parsed.hash, data.password);
var src = Hash.getBlobPathFromHex(secret.channel);
var key = Hash.encodeBase64(secret.keys.cryptKey);
var mt = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + key + '"></media-tag>';
editor.replaceSelection(mt);
}
};

Loading…
Cancel
Save