Merge branch 'master' into revised-german

pull/1/head
Paul Libbrecht 6 years ago committed by GitHub
commit 365d17e275
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

1
.gitignore vendored

@ -14,6 +14,7 @@ data
npm-debug.log npm-debug.log
pins/ pins/
blob/ blob/
block/
blobstage/ blobstage/
block/ block/
privileged.conf privileged.conf

@ -1,3 +1,34 @@
# Fossa release (v2.5.0)
## Goals
This release took longer than usual - three weeks instead of two - due to our plans involving a complete redesign of how login and registration function.
Any time we rework a critical system within CryptPad we're very cautious about deploying it, however, this update should bring considerable value for users.
From now on, users will be able to change their passwords without losing access to their old data, however, this is very different from _password recovery_.
While we will still be unable to help you if you have forgotten your password, this update will address our inability up until this point to change your password in the event that it has been compromised in some way.
## Update notes
* v2.5.0 uses newly released features in a clientside dependency ([chainpad-netflux](https://github.com/xwiki-labs/chainpad-netflux/releases/tag/0.7.2)). Run `bower update` to make sure you have the latest version.
* Update your server config to serve /block/ with maxAge 0d, if you are using a reverse proxy, or docker. `cryptpad/docs/example.nginx.conf` has been updated to include an example.
* Restart your server after updating.
* We have added a new feedback key, `NO_CSS_VARIABLES`, in order to diagnose how many of our clients support the CSS3 functionality.
### Features
* v2.5.0 introduces support for what we have called _modern users_.
* New registrations will use the new APIs that we've built to facillitate the ability to change your account password.
* _Legacy registrations_ will continue to function as they always have.
* Changing your password (via the settings page) will migrate old user accounts to the new system.
* We'll publish a blog post in the coming weeks to explain in depth how this functionality is implemented.
* The _kanban_ application now features support for export and import of your project data.
* This release features minor improvements to the _Deutsch_ translation
### Bug fixes
* We noticed that if you entered credentials for registration, and cancelled the displayed prompt informing you that such a user was already registered, the registration interface would not unlock for further interaction. This has been fixed.
* We found that on very slow connections, or when users opened pads in Firefox without focusing the tab, requirejs would fail to load dependencies before timing out. We've increased the timeout period by a factor of ten to address such cases.
# Echidna release (v2.4.0) # Echidna release (v2.4.0)
## Goals ## Goals

@ -8,13 +8,13 @@ RUN apk add --no-cache git tini \
&& npm install -g bower \ && npm install -g bower \
&& bower install --allow-root && bower install --allow-root
EXPOSE 3000 EXPOSE 3000 3001
VOLUME /cryptpad/datastore VOLUME /cryptpad/datastore
VOLUME /cryptpad/customize VOLUME /cryptpad/customize
ENV USE_SSL=false ENV USE_SSL=false
ENV STORAGE='./storage/file' ENV STORAGE=\'./storage/file\'
ENV LOG_TO_STDOUT=true ENV LOG_TO_STDOUT=true
CMD ["/sbin/tini", "--", "/cryptpad/container-start.sh"] CMD ["/sbin/tini", "--", "/cryptpad/container-start.sh"]

@ -0,0 +1,25 @@
FROM arm64v8/node:6
COPY . /cryptpad
WORKDIR /cryptpad
RUN npm config set unsafe-perm true
ADD https://github.com/krallin/tini/releases/download/v0.18.0/tini-static-arm64 /sbin/tini
RUN chmod a+x /sbin/tini
RUN apt install -y git \
&& npm install --production \
&& npm install -g bower \
&& bower install --allow-root
EXPOSE 3000 3001
VOLUME /cryptpad/datastore
VOLUME /cryptpad/customize
ENV USE_SSL=false
ENV STORAGE=\'./storage/file\'
ENV LOG_TO_STDOUT=true
CMD ["/sbin/tini", "--", "/cryptpad/container-start.sh"]

@ -211,6 +211,11 @@ module.exports = {
*/ */
taskPath: './tasks', taskPath: './tasks',
/* if you would like users' authenticated blocks to be stored in
a custom location, change the path below:
*/
blockPath: './block',
/* /*
* By default, CryptPad also contacts our accounts server once a day to check for changes in * By default, CryptPad also contacts our accounts server once a day to check for changes in
* the people who have accounts. This check-in will also send the version of your CryptPad * the people who have accounts. This check-in will also send the version of your CryptPad

@ -24,5 +24,5 @@ sedeasy() {
[ -n "$LOG_TO_STDOUT" ] && echo "Logging to stdout: $LOG_TO_STDOUT" \ [ -n "$LOG_TO_STDOUT" ] && echo "Logging to stdout: $LOG_TO_STDOUT" \
&& sedeasy "logToStdout: [^,]*," "logToStdout: ${LOG_TO_STDOUT}," customize/config.js && sedeasy "logToStdout: [^,]*," "logToStdout: ${LOG_TO_STDOUT}," customize/config.js
export FRESH=1
exec node ./server.js exec node ./server.js

@ -12,17 +12,23 @@ define([
'/common/common-feedback.js', '/common/common-feedback.js',
'/common/outer/local-store.js', '/common/outer/local-store.js',
'/customize/messages.js', '/customize/messages.js',
'/bower_components/nthen/index.js',
'/common/outer/login-block.js',
'/common/common-hash.js',
'/bower_components/tweetnacl/nacl-fast.min.js', '/bower_components/tweetnacl/nacl-fast.min.js',
'/bower_components/scrypt-async/scrypt-async.min.js', // better load speed '/bower_components/scrypt-async/scrypt-async.min.js', // better load speed
], function ($, Listmap, Crypto, Util, NetConfig, Cred, ChainPad, Realtime, Constants, UI, ], function ($, Listmap, Crypto, Util, NetConfig, Cred, ChainPad, Realtime, Constants, UI,
Feedback, LocalStore, Messages) { Feedback, LocalStore, Messages, nThen, Block, Hash) {
var Exports = { var Exports = {
Cred: Cred, Cred: Cred,
// this is depended on by non-customizable files
// be careful when modifying login.js
requiredBytes: 192,
}; };
var Nacl = window.nacl; var Nacl = window.nacl;
var allocateBytes = function (bytes) { var allocateBytes = Exports.allocateBytes = function (bytes) {
var dispense = Cred.dispenser(bytes); var dispense = Cred.dispenser(bytes);
var opt = {}; var opt = {};
@ -41,6 +47,10 @@ define([
// 32 more for a signing key // 32 more for a signing key
var edSeed = opt.edSeed = dispense(32); var edSeed = opt.edSeed = dispense(32);
// 64 more bytes to seed an additional signing key
var blockKeys = opt.blockKeys = Block.genkeys(new Uint8Array(dispense(64)));
opt.blockHash = Block.getBlockHash(blockKeys);
// derive a private key from the ed seed // derive a private key from the ed seed
var signingKeypair = Nacl.sign.keyPair.fromSeed(new Uint8Array(edSeed)); var signingKeypair = Nacl.sign.keyPair.fromSeed(new Uint8Array(edSeed));
@ -58,10 +68,24 @@ define([
// should never happen // should never happen
if (channelHex.length !== 32) { throw new Error('invalid channel id'); } if (channelHex.length !== 32) { throw new Error('invalid channel id'); }
opt.channel64 = Util.hexToBase64(channelHex); var channel64 = Util.hexToBase64(channelHex);
// we still generate a v1 hash because this function needs to deterministically
// derive the same values as it always has. New accounts will generate their own
// userHash values
opt.userHash = '/1/edit/' + [channel64, opt.keys.editKeyStr].join('/') + '/';
return opt;
};
opt.userHash = '/1/edit/' + [opt.channel64, opt.keys.editKeyStr].join('/');
var loginOptionsFromBlock = function (blockInfo) {
var opt = {};
var parsed = Hash.getSecrets('pad', blockInfo.User_hash);
opt.channelHex = parsed.channel;
opt.keys = parsed.keys;
opt.edPublic = blockInfo.edPublic;
opt.User_name = blockInfo.User_name;
return opt; return opt;
}; };
@ -92,6 +116,14 @@ define([
return Object.keys(proxy).length === 0; return Object.keys(proxy).length === 0;
}; };
var setMergeAnonDrive = function () {
sessionStorage.migrateAnonDrive = 1;
};
var setCreateReadme = function () {
sessionStorage.createReadme = 1;
};
Exports.loginOrRegister = function (uname, passwd, isRegister, shouldImport, cb) { Exports.loginOrRegister = function (uname, passwd, isRegister, shouldImport, cb) {
if (typeof(cb) !== 'function') { return; } if (typeof(cb) !== 'function') { return; }
@ -105,22 +137,101 @@ define([
return void cb('PASS_TOO_SHORT'); return void cb('PASS_TOO_SHORT');
} }
Cred.deriveFromPassphrase(uname, passwd, 128, function (bytes) { // results...
// results... var res = {
var res = { register: isRegister,
register: isRegister, };
};
var RT, blockKeys, blockHash, Pinpad, rpc, userHash;
nThen(function (waitFor) {
// derive a predefined number of bytes from the user's inputs,
// and allocate them in a deterministic fashion
Cred.deriveFromPassphrase(uname, passwd, Exports.requiredBytes, waitFor(function (bytes) {
res.opt = allocateBytes(bytes);
blockHash = res.opt.blockHash;
blockKeys = res.opt.blockKeys;
}));
}).nThen(function (waitFor) {
// the allocated bytes can be used either in a legacy fashion,
// or in such a way that a previously unused byte range determines
// the location of a layer of indirection which points users to
// an encrypted block, from which they can recover the location of
// the rest of their data
// determine where a block for your set of keys would be stored
var blockUrl = Block.getBlockUrl(res.opt.blockKeys);
// Check whether there is a block at that location
Util.fetch(blockUrl, waitFor(function (err, block) {
// if users try to log in or register, we must check
// whether there is a block.
// the block is only useful if it can be decrypted, though
if (err) {
console.log("no block found");
return;
}
var decryptedBlock = Block.decrypt(block, blockKeys);
if (!decryptedBlock) {
console.error("Found a login block but failed to decrypt");
return;
}
// run scrypt to derive the user's keys console.error(decryptedBlock);
var opt = res.opt = allocateBytes(bytes); res.blockInfo = decryptedBlock;
}));
}).nThen(function (waitFor) {
// we assume that if there is a block, it was created in a valid manner
// so, just proceed to the next block which handles that stuff
if (res.blockInfo) { return; }
// use the derived key to generate an object var opt = res.opt;
loadUserObject(opt, function (err, rt) {
// load the user's object using the legacy credentials
loadUserObject(opt, waitFor(function (err, rt) {
if (err) { return void cb(err); } if (err) { return void cb(err); }
// if a proxy is marked as deprecated, it is because someone had a non-owned drive
// but changed their password, and couldn't delete their old data.
// if they are here, they have entered their old credentials, so we should not
// allow them to proceed. In time, their old drive should get deleted, since
// it will should be pinned by anyone's drive.
if (rt.proxy[Constants.deprecatedKey]) {
return void cb('NO_SUCH_USER', res);
}
if (isRegister && isProxyEmpty(rt.proxy)) {
// If they are trying to register,
// and the proxy is empty, then there is no 'legacy user' either
// so we should just shut down this session and disconnect.
rt.network.disconnect();
return; // proceed to the next async block
}
// they tried to just log in but there's no such user
// and since we're here at all there is no modern-block
if (!isRegister && isProxyEmpty(rt.proxy)) {
rt.network.disconnect(); // clean up after yourself
waitFor.abort();
return void cb('NO_SUCH_USER', res);
}
// they tried to register, but those exact credentials exist
if (isRegister && !isProxyEmpty(rt.proxy)) {
rt.network.disconnect();
waitFor.abort();
Feedback.send('LOGIN', true);
return void cb('ALREADY_REGISTERED', res);
}
// if you are here, then there is no block, the user is trying
// to log in. The proxy is **not** empty. All values assigned here
// should have been deterministically created using their credentials
// so setting them is just a precaution to keep things in good shape
res.proxy = rt.proxy; res.proxy = rt.proxy;
res.realtime = rt.realtime; res.realtime = rt.realtime;
res.network = rt.network;
// they're registering... // they're registering...
res.userHash = opt.userHash; res.userHash = opt.userHash;
@ -130,53 +241,168 @@ define([
res.edPrivate = opt.edPrivate; res.edPrivate = opt.edPrivate;
res.edPublic = opt.edPublic; res.edPublic = opt.edPublic;
// export their encryption key
res.curvePrivate = opt.curvePrivate; res.curvePrivate = opt.curvePrivate;
res.curvePublic = opt.curvePublic; res.curvePublic = opt.curvePublic;
// they tried to just log in but there's no such user if (shouldImport) { setMergeAnonDrive(); }
// don't proceed past this async block.
waitFor.abort();
// We have to call whenRealtimeSyncs asynchronously here because in the current
// version of listmap, onLocal calls `chainpad.contentUpdate(newValue)`
// asynchronously.
// The following setTimeout is here to make sure whenRealtimeSyncs is called after
// `contentUpdate` so that we have an update userDoc in chainpad.
setTimeout(function () {
Realtime.whenRealtimeSyncs(rt.realtime, function () {
// the following stages are there to initialize a new drive
// if you are registering
LocalStore.login(res.userHash, res.userName, function () {
setTimeout(function () { cb(void 0, res); });
});
});
});
}));
}).nThen(function (waitFor) { // MODERN REGISTRATION / LOGIN
var opt;
if (res.blockInfo) {
opt = loginOptionsFromBlock(res.blockInfo);
userHash = res.blockInfo.User_hash;
console.error(opt, userHash);
} else {
console.log("allocating random bytes for a new user object");
opt = allocateBytes(Nacl.randomBytes(Exports.requiredBytes));
// create a random v2 hash, since we don't need backwards compatibility
userHash = opt.userHash = Hash.createRandomHash('drive');
var secret = Hash.getSecrets('drive', userHash);
opt.keys = secret.keys;
opt.channelHex = secret.channel;
}
// according to the location derived from the credentials which you entered
loadUserObject(opt, waitFor(function (err, rt) {
if (err) {
waitFor.abort();
return void cb('MODERN_REGISTRATION_INIT');
}
console.error(JSON.stringify(rt.proxy));
// export the realtime object you checked
RT = rt;
var proxy = rt.proxy;
if (isRegister && !isProxyEmpty(proxy) && (!proxy.edPublic || !proxy.edPrivate)) {
console.error("INVALID KEYS");
console.log(JSON.stringify(proxy));
return;
}
res.proxy = rt.proxy;
res.realtime = rt.realtime;
// they're registering...
res.userHash = userHash;
res.userName = uname;
// somehow they have a block present, but nothing in the user object it specifies
// this shouldn't happen, but let's send feedback if it does
if (!isRegister && isProxyEmpty(rt.proxy)) { if (!isRegister && isProxyEmpty(rt.proxy)) {
// this really shouldn't happen, but let's handle it anyway
Feedback.send('EMPTY_LOGIN_WITH_BLOCK');
rt.network.disconnect(); // clean up after yourself rt.network.disconnect(); // clean up after yourself
waitFor.abort();
return void cb('NO_SUCH_USER', res); return void cb('NO_SUCH_USER', res);
} }
// they tried to register, but those exact credentials exist // they tried to register, but those exact credentials exist
if (isRegister && !isProxyEmpty(rt.proxy)) { if (isRegister && !isProxyEmpty(rt.proxy)) {
rt.network.disconnect(); rt.network.disconnect();
waitFor.abort();
res.blockHash = blockHash;
if (shouldImport) {
setMergeAnonDrive();
}
return void cb('ALREADY_REGISTERED', res); return void cb('ALREADY_REGISTERED', res);
} }
if (isRegister) { if (!isRegister && !isProxyEmpty(rt.proxy)) {
var proxy = rt.proxy; LocalStore.setBlockHash(blockHash);
proxy.edPublic = res.edPublic; waitFor.abort();
proxy.edPrivate = res.edPrivate; if (shouldImport) {
proxy.curvePublic = res.curvePublic; setMergeAnonDrive();
proxy.curvePrivate = res.curvePrivate; }
return void LocalStore.login(userHash, uname, function () {
cb(void 0, res);
});
}
if (isRegister && isProxyEmpty(rt.proxy)) {
proxy.edPublic = opt.edPublic;
proxy.edPrivate = opt.edPrivate;
proxy.curvePublic = opt.curvePublic;
proxy.curvePrivate = opt.curvePrivate;
proxy.login_name = uname; proxy.login_name = uname;
proxy[Constants.displayNameKey] = uname; proxy[Constants.displayNameKey] = uname;
sessionStorage.createReadme = 1; setCreateReadme();
if (!shouldImport) { proxy.version = 6; } if (shouldImport) {
setMergeAnonDrive();
} else {
proxy.version = 6;
}
Feedback.send('REGISTRATION', true); Feedback.send('REGISTRATION', true);
} else { } else {
Feedback.send('LOGIN', true); Feedback.send('LOGIN', true);
} }
if (shouldImport) { setTimeout(waitFor(function () {
sessionStorage.migrateAnonDrive = 1; Realtime.whenRealtimeSyncs(rt.realtime, waitFor());
}));
}));
}).nThen(function (waitFor) {
require(['/common/pinpad.js'], waitFor(function (_Pinpad) {
console.log("loaded rpc module");
Pinpad = _Pinpad;
}));
}).nThen(function (waitFor) {
// send an RPC to store the block which you created.
console.log("initializing rpc interface");
Pinpad.create(RT.network, RT.proxy, waitFor(function (e, _rpc) {
if (e) {
waitFor.abort();
console.error(e); // INVALID_KEYS
return void cb('RPC_CREATION_ERROR');
} }
rpc = _rpc;
console.log("rpc initialized");
}));
}).nThen(function (waitFor) {
console.log("creating request to publish a login block");
// We have to call whenRealtimeSyncs asynchronously here because in the current // Finally, create the login block for the object you just created.
// version of listmap, onLocal calls `chainpad.contentUpdate(newValue)` var toPublish = {};
// asynchronously.
// The following setTimeout is here to make sure whenRealtimeSyncs is called after toPublish[Constants.userNameKey] = uname;
// `contentUpdate` so that we have an update userDoc in chainpad. toPublish[Constants.userHashKey] = userHash;
setTimeout(function () { toPublish.edPublic = RT.proxy.edPublic;
Realtime.whenRealtimeSyncs(rt.realtime, function () {
LocalStore.login(res.userHash, res.userName, function () { var blockRequest = Block.serialize(JSON.stringify(toPublish), res.opt.blockKeys);
setTimeout(function () { cb(void 0, res); });
}); rpc.writeLoginBlock(blockRequest, waitFor(function (e) {
}); if (e) { return void console.error(e); }
console.log("blockInfo available at:", blockHash);
LocalStore.setBlockHash(blockHash);
LocalStore.login(userHash, uname, function () {
cb(void 0, res);
}); });
}); }));
}); });
}; };
Exports.redirect = function () { Exports.redirect = function () {
@ -255,7 +481,6 @@ define([
}); });
break; break;
case 'ALREADY_REGISTERED': case 'ALREADY_REGISTERED':
// logMeIn should reset registering = false
UI.removeLoadingScreen(function () { UI.removeLoadingScreen(function () {
UI.confirm(Messages.register_alreadyRegistered, function (yes) { UI.confirm(Messages.register_alreadyRegistered, function (yes) {
if (!yes) { if (!yes) {
@ -268,6 +493,12 @@ define([
proxy[Constants.displayNameKey] = uname; proxy[Constants.displayNameKey] = uname;
} }
LocalStore.eraseTempSessionValues(); LocalStore.eraseTempSessionValues();
if (result.blockHash) {
LocalStore.setBlockHash(result.blockHash);
}
LocalStore.login(result.userHash, result.userName, function () { LocalStore.login(result.userHash, result.userName, function () {
setTimeout(function () { proceed(result); }); setTimeout(function () { proceed(result); });
}); });

@ -95,7 +95,7 @@ define([
]) ])
]) ])
]), ]),
h('div.cp-version-footer', "CryptPad v2.4.0 (Echidna)") h('div.cp-version-footer', "CryptPad v2.5.0 (Fossa)")
]); ]);
}; };

@ -39,15 +39,13 @@ define(function () {
out.padNotPinned = 'Dieses Dokument wird nach 3 Monaten ohne Zugang auslaufen, {0}logge Dich ein{1} or {2}registriere Dich{3}, um das Auslaufen zu verhindern.'; out.padNotPinned = 'Dieses Dokument wird nach 3 Monaten ohne Zugang auslaufen, {0}logge Dich ein{1} or {2}registriere Dich{3}, um das Auslaufen zu verhindern.';
out.anonymousStoreDisabled = "Der Webmaster dieses CryptPad Server hat die anonyme Verwendung deaktiviert. Du muss Dich einloggen, um CryptDrive zu verwenden."; out.anonymousStoreDisabled = "Der Webmaster dieses CryptPad Server hat die anonyme Verwendung deaktiviert. Du muss Dich einloggen, um CryptDrive zu verwenden.";
out.expiredError = 'Dieses Dokument ist abgelaufen und ist nicht mehr verfügbar.'; out.expiredError = 'Dieses Dokument ist abgelaufen und ist nicht mehr verfügbar.';
out.deletedError = 'Dieses Dokument wurde von seinem Besitzer gelöscht und ist nicht mehr verfügbar.'; out.deletedError = 'Dieses Dokument wurde von seinem Besitzer gelöscht und nicht mehr verfügbar.';
out.inactiveError = 'Dieses Dokument ist wegen Inaktivität gelöscht worden. Drücke auf die Esc-Taste, um ein neues Dokument zu erstellen.'; out.inactiveError = 'Dieses Dokument ist wegen Inaktivität gelöscht worden. Drucke auf die Esc-Taste, um ein neues Dokument zu gestalten.';
out.chainpadError = 'Ein kritischer Fehler ist beim Aktualisieren Deines Dokuments aufgetreten. Dieses Dokument ist schreibgeschützt, damit Du sicherstellen kannst, dass kein Inhalt verloren geht.<br>'+ out.chainpadError = 'Ein kritischer Fehler hat stattgefunden, bei den Updates deines Dokuments. Dieses Dokument ist schreibgeschützt, damit du sicher machen kannst, dass keine Inhalt verloren geht.<br>'+
'Drücke auf <em>Esc</em>, um das Dokument schreibgeschützt zu lesen, oder lade es neu, um das Editierien wieder aufzunehmen.'; 'Druck auf <em>Esc</em>, um das Dokument schreibgeschützt zu lesen, oder lade es neu, um das Editierien wiederanzufangen.';
out.errorCopy = ' Du kannst noch den Inhalt woanders hin kopieren, nachdem Du <em>Esc</em> gedrückt hast.<br>Wenn Du die Seite verlässt, verschwindet der Inhalt für immer!'; out.errorCopy = ' Du kannst noch den Inhalt woanders kopieren, nachdem du <em>Esc</em> drucken.<br>Wenn du die Seite verlässt, verschwindet der Inhalt für immer!';
out.errorRedirectToHome = 'Drückee <em>Esc</em> um zu Deinem CryptDrive zurückzukehren.'; out.errorRedirectToHome = 'Drucke <em>Esc</em>, um zu deinem CryptDrive zu gehen.';
out.newVersionError = "Eine neue Version von CryptPad ist verfügbar.<br>" +
"<a href='#'>Lade die Seite neu</a> um die neue version zu benutzen, oder drücke Esc um im <b>Offline-Modus</b> weiterzuarbeiten.";
out.loading = "Laden..."; out.loading = "Laden...";
out.error = "Fehler"; out.error = "Fehler";
out.saved = "Gespeichert"; out.saved = "Gespeichert";

@ -49,7 +49,7 @@ define(function () {
out.deleted = "Pad supprimé de votre CryptDrive"; out.deleted = "Pad supprimé de votre CryptDrive";
out.deletedFromServer = "Pad supprimé du serveur"; out.deletedFromServer = "Pad supprimé du serveur";
out.realtime_unrecoverableError = "Le moteur temps-réel a rencontré une erreur critique. Cliquez sur OK pour recharger la page."; out.realtime_unrecoverableError = "Une erreur critique est survenue. Cliquez sur OK pour recharger la page.";
out.disconnected = 'Déconnecté'; out.disconnected = 'Déconnecté';
out.synchronizing = 'Synchronisation'; out.synchronizing = 'Synchronisation';
@ -235,7 +235,6 @@ define(function () {
out.history_next = "Version plus récente"; out.history_next = "Version plus récente";
out.history_prev = "Version plus ancienne"; out.history_prev = "Version plus ancienne";
out.history_loadMore = "Charger davantage d'historique"; out.history_loadMore = "Charger davantage d'historique";
out.history_close = "Retour";
out.history_closeTitle = "Fermer l'historique"; out.history_closeTitle = "Fermer l'historique";
out.history_restoreTitle = "Restaurer la version du document sélectionnée"; out.history_restoreTitle = "Restaurer la version du document sélectionnée";
out.history_restorePrompt = "Êtes-vous sûr de vouloir remplacer la version actuelle du document par la version affichée ?"; out.history_restorePrompt = "Êtes-vous sûr de vouloir remplacer la version actuelle du document par la version affichée ?";
@ -597,6 +596,17 @@ define(function () {
out.settings_templateSkip = "Passer la fenêtre de choix d'un modèle"; out.settings_templateSkip = "Passer la fenêtre de choix d'un modèle";
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.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.settings_changePasswordTitle = "Changer de mot de passe";
out.settings_changePasswordHint = "Pour modifier le mot de passe de votre compte utilisateur, entrez votre mot de passe actuel et confirmez le nouveau mot de passe en la tapant deux fois.<br>" +
"<b>Nous ne pouvons pas réinitialiser votre mot de passe si vous le perdez, donc soyez très prudent !</b>";
out.settings_changePasswordButton = "Changer le mot de passe";
out.settings_changePasswordCurrent = "Mot de passe actuel";
out.settings_changePasswordNew = "Nouveau mot de passe";
out.settings_changePasswordNewConfirm = "Confirmer le nouveau mot de passe";
out.settings_changePasswordConfirm = "Êtes-vous sûr de vouloir changer votre mot de passe ? Vous devrez vous reconnecter sur tous vos appareils.";
out.settings_changePasswordError = "Une erreur est survenue. Si vous n'êtes plus en mesure de vous connecter à votre compte utilisateur ou de changer votre mot de passe, veuillez contacter l'administrateur de votre CryptPad.";
out.settings_changePasswordPending = "Votre mot de passe est en train d'être modifié. Veuillez ne pas fermer ou recharger cette page avant que le traitement soit terminé.";
out.upload_title = "Hébergement de fichiers"; out.upload_title = "Hébergement de fichiers";
out.upload_modal_title = "Options d'importation du fichier"; out.upload_modal_title = "Options d'importation du fichier";
out.upload_modal_filename = "Nom (extension <em>{0}</em> ajoutée automatiquement)"; out.upload_modal_filename = "Nom (extension <em>{0}</em> ajoutée automatiquement)";
@ -707,7 +717,7 @@ define(function () {
out.whatis_drive_p2 = "Avec le glisser-déposer intuitif, vous pouvez déplacer vos pads dans votre drive tout en conservant les liens vers ces pads pour que vos collaborateurs n'en perdent pas l'accès"; out.whatis_drive_p2 = "Avec le glisser-déposer intuitif, vous pouvez déplacer vos pads dans votre drive tout en conservant les liens vers ces pads pour que vos collaborateurs n'en perdent pas l'accès";
out.whatis_drive_p3 = "Vous pouvez également importer des fichiers dans votre CryptDrive et les partager avec des collègues. Les fichiers importés peuvent être rangés de la même manière que vos pads collaboratifs."; out.whatis_drive_p3 = "Vous pouvez également importer des fichiers dans votre CryptDrive et les partager avec des collègues. Les fichiers importés peuvent être rangés de la même manière que vos pads collaboratifs.";
out.whatis_business = 'CryptPad for Business'; out.whatis_business = 'CryptPad for Business';
out.whatis_business_p1 = "Le chiffrement Zero Knowledge de CryptPad excelle pour multiplier l'efficacité des protocoles de sécurité existants en recréant les contrôles d'accès organisationnels de manière cryptographique. Puisque les données sensibles ne peuvent être déchiffrées qu'en utilisant les identifiants d'un employé, CryptPad empêche d'éventuels hackers ayant réussi à s'introduire dans le serveur d'avoir accès en clair à ces données. Découvrez-en plus sur la manière dont CryptPad peut aider votre entreprise en lisant le <a href=\"https://blog.cryptpad.fr/images/CryptPad-Whitepaper-v1.0.pdf\">CryptPad Whitepaper</a>."; out.whatis_business_p1 = "Le chiffrement Zero Knowledge de CryptPad excelle pour accroître l'efficacité des protocoles de sécurité existants en les recréant de manière cryptographique. Puisque les données sensibles ne peuvent être déchiffrées qu'en utilisant les identifiants d'un utilisateur, CryptPad empêche d'éventuels hackers ayant réussi à s'introduire dans le serveur d'avoir accès en clair à ces données. Découvrez-en plus sur la manière dont CryptPad peut aider votre entreprise en lisant le <a href=\"https://blog.cryptpad.fr/images/CryptPad-Whitepaper-v1.0.pdf\">CryptPad Whitepaper</a>.";
out.whatis_business_p2 = "CryptPad est déployable sur site et les <a href=\"https://cryptpad.fr/about.html\">développeurs CryptPad</a> chez XWiki SAS peuvent effectuer du développement, des personnalisations et du support commercial. Contactez-nous à <a href=\"mailto:sales@cryptpad.fr\">sales@cryptpad.fr</a> pour plus d'informations."; out.whatis_business_p2 = "CryptPad est déployable sur site et les <a href=\"https://cryptpad.fr/about.html\">développeurs CryptPad</a> chez XWiki SAS peuvent effectuer du développement, des personnalisations et du support commercial. Contactez-nous à <a href=\"mailto:sales@cryptpad.fr\">sales@cryptpad.fr</a> pour plus d'informations.";
// privacy.html // privacy.html
@ -802,6 +812,10 @@ define(function () {
"Les pads existant dans votre CryptDrive peuvent être transformés en tant que modèle en les déplaçant dans la catégorie <em>Modèles</em> du CryptDrive.<br>" + "Les pads existant dans votre CryptDrive peuvent être transformés en tant que modèle en les déplaçant dans la catégorie <em>Modèles</em> du CryptDrive.<br>" +
"Il est également possible de créer une copie d'un pad en tant que modèle en cliquant sur le bouton <span class=\"fa fa-bookmark\"></span> (<em>Sauver en tant que modèle</em>) dans la barre d'outils des éditeurs." "Il est également possible de créer une copie d'un pad en tant que modèle en cliquant sur le bouton <span class=\"fa fa-bookmark\"></span> (<em>Sauver en tant que modèle</em>) dans la barre d'outils des éditeurs."
}, },
abandoned: {
q: "Qu'est-ce qu'un pad abandonné?",
a: "Un <em>pad abandonné</em> est un pad qui n'est stocké dans le CryptDrive d'aucun utilisateur enregistré et qui n'a pas été modifié depuis 6 mois. Les documents abandonnées sont automatiquement supprimés du serveur."
},
}; };
out.faq.privacy = { out.faq.privacy = {
title: 'Confidentialité', title: 'Confidentialité',
@ -824,7 +838,7 @@ define(function () {
"Les formulaires d'inscription et de connexion génèrent à la place un ensemble de clés uniques, créées à partir de vos identifiants, et le serveur ne connaît donc que votre signature cryptographique.<br>" + "Les formulaires d'inscription et de connexion génèrent à la place un ensemble de clés uniques, créées à partir de vos identifiants, et le serveur ne connaît donc que votre signature cryptographique.<br>" +
"Nous utilisons cette information principalement pour mesurer combien de données vous avez stocké sur nos serveurs, afin de pouvoir limiter chaque utilisateur à son quota.<br><br>" + "Nous utilisons cette information principalement pour mesurer combien de données vous avez stocké sur nos serveurs, afin de pouvoir limiter chaque utilisateur à son quota.<br><br>" +
"Nous utilisons également notre fonctionnalité de <em>retour d'expérience</em> pour indiquer au serveur que quelqu'un avec votre adresse IP a créé un compte utilisateur, bien que nous ne sachions pas lequel. Cela nous permet de mesurer le nombre d'inscriptions sur CryptPad mais aussi de voir dans quelles régions du monde se trouvent les utilisateurs, afin de déterminer les langues dans lesquelles traduire CryptPad.<br><br>" + "Nous utilisons également notre fonctionnalité de <em>retour d'expérience</em> pour indiquer au serveur que quelqu'un avec votre adresse IP a créé un compte utilisateur, bien que nous ne sachions pas lequel. Cela nous permet de mesurer le nombre d'inscriptions sur CryptPad mais aussi de voir dans quelles régions du monde se trouvent les utilisateurs, afin de déterminer les langues dans lesquelles traduire CryptPad.<br><br>" +
"Enfin, les clés générées à l'inscription permettent d'indiquer au serveur que les pads dans votre CryptDrive ne doivent pas être supprimés, même s'ils sont inactifs. Ce système a l'inconvénient de nous fournir davantage d'informations sur la façon dont vous utilisez CryptPad, mais il est nécessaire pour que nous puissions supprimer du serveur les pads inactifs dont personne n'a besoin." "Enfin, les utilisateurs enregistrés indiquent au serveur quels pads sont dans leur CryptDrive, afin que ces pads ne soient pas considérés comme abandonnés et ne soient donc pas supprimés pour inactivité."
}, },
other: { other: {
q: "Que peuvent apprendre les autres collaborateurs à mon sujet ?", q: "Que peuvent apprendre les autres collaborateurs à mon sujet ?",
@ -1121,6 +1135,7 @@ define(function () {
out.properties_changePassword = "Modifier le mot de passe"; out.properties_changePassword = "Modifier le mot de passe";
out.properties_confirmNew = "Êtes-vous sûr ? Ajouter un mot de passe changera l'URL de ce pad et supprimera son historique. Les utilisateurs ne connaissant pas le nouveau mot de passe perdront l'accès au pad."; out.properties_confirmNew = "Êtes-vous sûr ? Ajouter un mot de passe changera l'URL de ce pad et supprimera son historique. Les utilisateurs ne connaissant pas le nouveau mot de passe perdront l'accès au pad.";
out.properties_confirmChange = "Êtes-vous sûr ? Changer le mot de passe supprimera l'historique de ce pad. Les utilisateurs ne connaissant pas le nouveau mot de passe perdront l'accès au pad."; out.properties_confirmChange = "Êtes-vous sûr ? Changer le mot de passe supprimera l'historique de ce pad. Les utilisateurs ne connaissant pas le nouveau mot de passe perdront l'accès au pad.";
out.properties_passwordSame = "Le nouveau mot de passe doit être différent de celui existant.";
out.properties_passwordError = "Une erreur est survenue lors de la modification du mot de passe. Veuillez réessayer."; out.properties_passwordError = "Une erreur est survenue lors de la modification du mot de passe. Veuillez réessayer.";
out.properties_passwordWarning = "Le mot de passe a été modifié avec succès mais nous n'avons pas réussi à mettre à jour votre CryptDrive avec les nouvelles informations. Vous devrez peut-être supprimer manuellement l'ancienne version de ce pad.<br>Appuyez sur OK pour recharger le pad et mettre à jour vos droits d'accès."; out.properties_passwordWarning = "Le mot de passe a été modifié avec succès mais nous n'avons pas réussi à mettre à jour votre CryptDrive avec les nouvelles informations. Vous devrez peut-être supprimer manuellement l'ancienne version de ce pad.<br>Appuyez sur OK pour recharger le pad et mettre à jour vos droits d'accès.";
out.properties_passwordSuccess = "Le mot de passe a été modifié avec succès.<br>Appuyez sur OK pour mettre à jour vos droits d'accès."; out.properties_passwordSuccess = "Le mot de passe a été modifié avec succès.<br>Appuyez sur OK pour mettre à jour vos droits d'accès.";

@ -50,7 +50,7 @@ define(function () {
out.deleted = "Pad deleted from your CryptDrive"; out.deleted = "Pad deleted from your CryptDrive";
out.deletedFromServer = "Pad deleted from the server"; out.deletedFromServer = "Pad deleted from the server";
out.realtime_unrecoverableError = "The realtime engine has encountered an unrecoverable error. Click OK to reload."; out.realtime_unrecoverableError = "An unrecoverable error has occured. Click OK to reload.";
out.disconnected = 'Disconnected'; out.disconnected = 'Disconnected';
out.synchronizing = 'Synchronizing'; out.synchronizing = 'Synchronizing';
@ -600,14 +600,21 @@ define(function () {
out.settings_templateSkip = "Skip the template selection modal"; out.settings_templateSkip = "Skip the template selection modal";
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.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.settings_changePasswordTitle = "Change your password"; // XXX out.settings_ownDriveTitle = "Drive migration"; // XXX
out.settings_changePasswordHint = "Change your account's password without losing its data. You have to enter your existing password once, and the new password you want twice.<br>" + out.settings_ownDriveHint = "Migrating your drive to the new version will give you access to new features..."; // XXX
"<b>We can't reset your password if you forget it so be very careful!</b>"; // XXX out.settings_ownDriveButton = "Migrate"; // XXX
out.settings_changePasswordButton = "Change password"; // XXX out.settings_ownDriveConfirm = "Are you sure?"; // XXX
out.settings_changePasswordCurrent = "Existing password"; // XXX
out.settings_changePasswordNew = "New password"; // XXX out.settings_changePasswordTitle = "Change your password";
out.settings_changePasswordNewConfirm = "Confirm new password"; // XXX out.settings_changePasswordHint = "Change your account's password. Enter your current password, and confirm the new password by typing it twice.<br>" +
out.settings_changePasswordConfirm = "Are you sure?"; // XXX "<b>We can't reset your password if you forget it, so be very careful!</b>";
out.settings_changePasswordButton = "Change password";
out.settings_changePasswordCurrent = "Current password";
out.settings_changePasswordNew = "New password";
out.settings_changePasswordNewConfirm = "Confirm new password";
out.settings_changePasswordConfirm = "Are you sure you want to change your password? You will need to log back in on all your devices.";
out.settings_changePasswordError = "An unexpected error occurred. If you are unable to login or change your password, contact your CryptPad administrators.";
out.settings_changePasswordPending = "Your password is being updated. Please do not close or reload this page until the process has completed.";
out.upload_title = "File upload"; out.upload_title = "File upload";
out.upload_modal_title = "File upload options"; out.upload_modal_title = "File upload options";
@ -720,7 +727,7 @@ define(function () {
out.whatis_drive_p2 = 'With intuitive drag-and-drop, you can move pads around in your drive and the link to these pads will stay the same so your collaborators will never lose access.'; out.whatis_drive_p2 = 'With intuitive drag-and-drop, you can move pads around in your drive and the link to these pads will stay the same so your collaborators will never lose access.';
out.whatis_drive_p3 = 'You can also upload files in your CryptDrive and share them with colleagues. Uploaded files can be organized just like collaborative pads.'; out.whatis_drive_p3 = 'You can also upload files in your CryptDrive and share them with colleagues. Uploaded files can be organized just like collaborative pads.';
out.whatis_business = 'CryptPad for Business'; out.whatis_business = 'CryptPad for Business';
out.whatis_business_p1 = 'CryptPad\'s Zero Knowledge encryption is excellent for multiplying the effectiveness of existing security protocols by mirroring organizational access controls in cryptography. Because sensitive assets can only be decrypted using employee access credentials, CryptPad removes the hacker jackpot which exists in traditional IT servers. Read the <a href="https://blog.cryptpad.fr/images/CryptPad-Whitepaper-v1.0.pdf">CryptPad Whitepaper</a> to learn more about how it can help your business.'; out.whatis_business_p1 = "CryptPad\'s Zero Knowledge encryption multiplies the effectiveness of existing security protocols by mirroring organizational access controls in cryptography. Because sensitive assets can only be decrypted using user access credentials, CryptPad is less valuable as a target when compared to traditional cloud services. Read the <a href='https://blog.cryptpad.fr/images/CryptPad-Whitepaper-v1.0.pdf'>CryptPad Whitepaper</a> to learn more about how it can help your business.";
out.whatis_business_p2 = 'CryptPad is deployable on premises and the <a href="https://cryptpad.fr/about.html">CryptPad developers</a> at XWiki SAS are able to offer commercial support, customization and development. Reach out to <a href="mailto:sales@cryptpad.fr">sales@cryptpad.fr</a> for more information.'; out.whatis_business_p2 = 'CryptPad is deployable on premises and the <a href="https://cryptpad.fr/about.html">CryptPad developers</a> at XWiki SAS are able to offer commercial support, customization and development. Reach out to <a href="mailto:sales@cryptpad.fr">sales@cryptpad.fr</a> for more information.';
// privacy.html // privacy.html
@ -817,6 +824,10 @@ define(function () {
" Any existing pad can be turned into a template by moving it into the <em>Templates</em> section in your CryptDrive." + " Any existing pad can be turned into a template by moving it into the <em>Templates</em> section in your CryptDrive." +
" You can also create a copy of a pad to be used as a template by clicking the template button (<span class='fa fa-bookmark'></span>) in the editor's toolbar." " You can also create a copy of a pad to be used as a template by clicking the template button (<span class='fa fa-bookmark'></span>) in the editor's toolbar."
}, },
abandoned: {
q: "What is an abandoned pad?",
a: "An <em>abandoned pad</em> is a pad that is not pinned in any registered user's CryptDrive and that hasn't been changed for six months. Abandoned documents will be automatically removed from the server."
},
}; };
out.faq.privacy = { out.faq.privacy = {
title: 'Privacy', title: 'Privacy',
@ -847,8 +858,7 @@ define(function () {
"We use our <em>feedback</em> functionality to inform the server that someone with your IP has registered an account." + "We use our <em>feedback</em> functionality to inform the server that someone with your IP has registered an account." +
" We use this to measure how many people register for CryptPad accounts, and to see what regions they are in so that we can guess which languages may need better support.<br><br>" + " We use this to measure how many people register for CryptPad accounts, and to see what regions they are in so that we can guess which languages may need better support.<br><br>" +
"When you register, you generate a public key which is used to tell the server that the pads in your CryptDrive should not be deleted even if they are not actively being used." + "Registered users inform the server which pads are in their CryptDrive so that such pads are not considered abandoned, and are removed from the server due to inactivity."
" This information does reveal more about how you are using CryptPad, but the system allows us to remove pads from the server once nobody cares enough to keep them."
}, },
other: { other: {
q: "What can other collaborators learn about me?", q: "What can other collaborators learn about me?",
@ -1174,6 +1184,7 @@ define(function () {
out.properties_changePassword = "Change the password"; out.properties_changePassword = "Change the password";
out.properties_confirmNew = "Are you sure? Adding a password will change this pad's URL and remove its history. Users without the password will lose access to this pad"; out.properties_confirmNew = "Are you sure? Adding a password will change this pad's URL and remove its history. Users without the password will lose access to this pad";
out.properties_confirmChange = "Are you sure? Changing the password will remove its history. Users without the new password will lose access to this pad"; out.properties_confirmChange = "Are you sure? Changing the password will remove its history. Users without the new password will lose access to this pad";
out.properties_passwordSame = "New passwords must differ from the current one.";
out.properties_passwordError = "An error occured while trying to change the password. Please try again."; out.properties_passwordError = "An error occured while trying to change the password. Please try again.";
out.properties_passwordWarning = "The password was successfully changed but we were unable to update your CryptDrive with the new data. You may have to remove the old version of the pad manually.<br>Press OK to reload and update your acces rights."; out.properties_passwordWarning = "The password was successfully changed but we were unable to update your CryptDrive with the new data. You may have to remove the old version of the pad manually.<br>Press OK to reload and update your acces rights.";
out.properties_passwordSuccess = "The password was successfully changed.<br>Press OK to reload and update your access rights."; out.properties_passwordSuccess = "The password was successfully changed.<br>Press OK to reload and update your access rights.";

@ -85,6 +85,11 @@ server {
try_files $uri =404; try_files $uri =404;
} }
location ^~ /block/ {
add_header Cache-Control max-age=0;
try_files $uri =404;
}
location ^~ /datastore/ { location ^~ /datastore/ {
add_header Cache-Control max-age=0; add_header Cache-Control max-age=0;
try_files $uri =404; try_files $uri =404;

@ -1,7 +1,7 @@
{ {
"name": "cryptpad", "name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server", "description": "realtime collaborative visual editor with zero knowlege server",
"version": "2.4.0", "version": "2.5.0",
"license": "AGPL-3.0+", "license": "AGPL-3.0+",
"repository": { "repository": {
"type": "git", "type": "git",

176
rpc.js

@ -25,7 +25,7 @@ var SUPPRESS_RPC_ERRORS = false;
var WARN = function (e, output) { var WARN = function (e, output) {
if (!SUPPRESS_RPC_ERRORS && e && output) { if (!SUPPRESS_RPC_ERRORS && e && output) {
console.error(new Date().toISOString() + ' [' + e + ']', output); console.error(new Date().toISOString() + ' [' + String(e) + ']', output);
console.error(new Error(e).stack); console.error(new Error(e).stack);
console.error(); console.error();
} }
@ -1297,6 +1297,159 @@ var upload_status = function (Env, publicKey, filesize, cb) {
}); });
}; };
/*
We assume that the server is secured against MitM attacks
via HTTPS, and that malicious actors do not have code execution
capabilities. If they do, we have much more serious problems.
The capability to replay a block write or remove results in either
a denial of service for the user whose block was removed, or in the
case of a write, a rollback to an earlier password.
Since block modification is destructive, this can result in loss
of access to the user's drive.
So long as the detached signature is never observed by a malicious
party, and the server discards it after proof of knowledge, replays
are not possible. However, this precludes verification of the signature
at a later time.
Despite this, an integrity check is still possible by the original
author of the block, since we assume that the block will have been
encrypted with xsalsa20-poly1305 which is authenticated.
*/
var validateLoginBlock = function (Env, publicKey, signature, block, cb) {
// convert the public key to a Uint8Array and validate it
if (typeof(publicKey) !== 'string') { return void cb('E_INVALID_KEY'); }
var u8_public_key;
try {
u8_public_key = Nacl.util.decodeBase64(publicKey);
} catch (e) {
return void cb('E_INVALID_KEY');
}
var u8_signature;
try {
u8_signature = Nacl.util.decodeBase64(signature);
} catch (e) {
console.error(e);
return void cb('E_INVALID_SIGNATURE');
}
// convert the block to a Uint8Array
var u8_block;
try {
u8_block = Nacl.util.decodeBase64(block);
} catch (e) {
return void cb('E_INVALID_BLOCK');
}
// take its hash
var hash = Nacl.hash(u8_block);
// validate the signature against the hash of the content
var verified = Nacl.sign.detached.verify(hash, u8_signature, u8_public_key);
// existing authentication ensures that users cannot replay old blocks
// call back with (err) if unsuccessful
if (!verified) { return void cb("E_COULD_NOT_VERIFY"); }
return void cb(null, u8_block);
};
var createLoginBlockPath = function (Env, publicKey) {
// prepare publicKey to be used as a file name
var safeKey = escapeKeyCharacters(publicKey);
// validate safeKey
if (typeof(safeKey) !== 'string') {
return;
}
// derive the full path
// /home/cryptpad/cryptpad/block/fg/fg32kefksjdgjkewrjksdfksjdfsdfskdjfsfd
return Path.join(Env.paths.block, safeKey.slice(0, 2), safeKey);
};
var writeLoginBlock = function (Env, msg, cb) {
//console.log(msg);
var publicKey = msg[0];
var signature = msg[1];
var block = msg[2];
validateLoginBlock(Env, publicKey, signature, block, function (e, validatedBlock) {
if (e) { return void cb(e); }
if (!(validatedBlock instanceof Uint8Array)) { return void cb('E_INVALID_BLOCK'); }
// derive the filepath
var path = createLoginBlockPath(Env, publicKey);
// make sure the path is valid
if (typeof(path) !== 'string') {
return void cb('E_INVALID_BLOCK_PATH');
}
var parsed = Path.parse(path);
if (!parsed || typeof(parsed.dir) !== 'string') {
return void cb("E_INVALID_BLOCK_PATH_2");
}
nThen(function (w) {
// make sure the path to the file exists
Mkdirp(parsed.dir, w(function (e) {
if (e) {
w.abort();
cb(e);
}
}));
}).nThen(function () {
// actually write the block
// flow is dumb and I need to guard against this which will never happen
/*:: if (typeof(validatedBlock) === 'undefined') { throw new Error('should never happen'); } */
/*:: if (typeof(path) === 'undefined') { throw new Error('should never happen'); } */
Fs.writeFile(path, new Buffer(validatedBlock), { encoding: "binary", }, function (err) {
if (err) { return void cb(err); }
cb();
});
});
});
};
/*
When users write a block, they upload the block, and provide
a signature proving that they deserve to be able to write to
the location determined by the public key.
When removing a block, there is nothing to upload, but we need
to sign something. Since the signature is considered sensitive
information, we can just sign some constant and use that as proof.
*/
var removeLoginBlock = function (Env, msg, cb) {
var publicKey = msg[0];
var signature = msg[1];
var block = Nacl.util.decodeUTF8('DELETE_BLOCK'); // clients and the server will have to agree on this constant
validateLoginBlock(Env, publicKey, signature, block, function (e /*::, validatedBlock */) {
if (e) { return void cb(e); }
// derive the filepath
var path = createLoginBlockPath(Env, publicKey);
// make sure the path is valid
if (typeof(path) !== 'string') {
return void cb('E_INVALID_BLOCK_PATH');
}
Fs.unlink(path, function (err) {
if (err) { return void cb(err); }
cb();
});
});
};
var isNewChannel = function (Env, channel, cb) { var isNewChannel = function (Env, channel, cb) {
if (!isValidId(channel)) { return void cb('INVALID_CHAN'); } if (!isValidId(channel)) { return void cb('INVALID_CHAN'); }
if (channel.length !== 32) { return void cb('INVALID_CHAN'); } if (channel.length !== 32) { return void cb('INVALID_CHAN'); }
@ -1353,6 +1506,8 @@ var isAuthenticatedCall = function (call) {
'CLEAR_OWNED_CHANNEL', 'CLEAR_OWNED_CHANNEL',
'REMOVE_OWNED_CHANNEL', 'REMOVE_OWNED_CHANNEL',
'REMOVE_PINS', 'REMOVE_PINS',
'WRITE_LOGIN_BLOCK',
'REMOVE_LOGIN_BLOCK',
].indexOf(call) !== -1; ].indexOf(call) !== -1;
}; };
@ -1423,6 +1578,7 @@ RPC.create = function (
var pinPath = paths.pin = keyOrDefaultString('pinPath', './pins'); var pinPath = paths.pin = keyOrDefaultString('pinPath', './pins');
var blobPath = paths.blob = keyOrDefaultString('blobPath', './blob'); var blobPath = paths.blob = keyOrDefaultString('blobPath', './blob');
var blobStagingPath = paths.staging = keyOrDefaultString('blobStagingPath', './blobstage'); var blobStagingPath = paths.staging = keyOrDefaultString('blobStagingPath', './blobstage');
paths.block = keyOrDefaultString('blockPath', './block');
var isUnauthenticateMessage = function (msg) { var isUnauthenticateMessage = function (msg) {
return msg && msg.length === 2 && isUnauthenticatedCall(msg[0]); return msg && msg.length === 2 && isUnauthenticatedCall(msg[0]);
@ -1559,7 +1715,7 @@ RPC.create = function (
var session = Sessions[safeKey]; var session = Sessions[safeKey];
var token = session? session.tokens.slice(-1)[0]: ''; var token = session? session.tokens.slice(-1)[0]: '';
var cookie = makeCookie(token).join('|'); var cookie = makeCookie(token).join('|');
respond(e, [cookie].concat(typeof(msg) !== 'undefined' ?msg: [])); respond(e ? String(e): e, [cookie].concat(typeof(msg) !== 'undefined' ?msg: []));
}; };
if (typeof(msg) !== 'object' || !msg.length) { if (typeof(msg) !== 'object' || !msg.length) {
@ -1692,6 +1848,22 @@ RPC.create = function (
WARN(e, 'UPLOAD_CANCEL'); WARN(e, 'UPLOAD_CANCEL');
Respond(e); Respond(e);
}); });
case 'WRITE_LOGIN_BLOCK':
return void writeLoginBlock(Env, msg[1], function (e) {
if (e) {
WARN(e, 'WRITE_LOGIN_BLOCK');
return void Respond(e);
}
Respond(e);
});
case 'REMOVE_LOGIN_BLOCK':
return void removeLoginBlock(Env, msg[1], function (e) {
if (e) {
WARN(e, 'REMOVE_LOGIN_BLOCK');
return void Respond(e);
}
Respond(e);
});
default: default:
return void Respond('UNSUPPORTED_RPC_CALL', msg); return void Respond('UNSUPPORTED_RPC_CALL', msg);
} }

@ -126,6 +126,9 @@ app.use("/blob", Express.static(Path.join(__dirname, (config.blobPath || './blob
app.use("/datastore", Express.static(Path.join(__dirname, (config.filePath || './datastore')), { app.use("/datastore", Express.static(Path.join(__dirname, (config.filePath || './datastore')), {
maxAge: "0d" maxAge: "0d"
})); }));
app.use("/block", Express.static(Path.join(__dirname, (config.blockPath || '/block')), {
maxAge: "0d",
}));
app.use("/customize", Express.static(__dirname + '/customize')); app.use("/customize", Express.static(__dirname + '/customize'));
app.use("/customize", Express.static(__dirname + '/customize.dist')); app.use("/customize", Express.static(__dirname + '/customize.dist'));
@ -162,7 +165,7 @@ app.get('/api/config', function(req, res){
res.send('define(function(){\n' + [ res.send('define(function(){\n' + [
'var obj = ' + JSON.stringify({ 'var obj = ' + JSON.stringify({
requireConf: { requireConf: {
waitSeconds: 60, waitSeconds: 600,
urlArgs: 'ver=' + Package.version + (FRESH_KEY? '-' + FRESH_KEY: '') + (DEV_MODE? '-' + (+new Date()): ''), urlArgs: 'ver=' + Package.version + (FRESH_KEY? '-' + FRESH_KEY: '') + (DEV_MODE? '-' + (+new Date()): ''),
}, },
removeDonateButton: (config.removeDonateButton === true), removeDonateButton: (config.removeDonateButton === true),

@ -10,9 +10,13 @@ define([
'/common/wire.js', '/common/wire.js',
'/common/flat-dom.js', '/common/flat-dom.js',
'/common/media-tag.js', '/common/media-tag.js',
], function ($, Hyperjson, Sortify, Drive, Test, Hash, Util, Thumb, Wire, Flat, MediaTag) { '/common/outer/login-block.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
], function ($, Hyperjson, Sortify, Drive, Test, Hash, Util, Thumb, Wire, Flat, MediaTag, Block) {
window.Hyperjson = Hyperjson; window.Hyperjson = Hyperjson;
window.Sortify = Sortify; window.Sortify = Sortify;
var Nacl = window.nacl;
var assertions = 0; var assertions = 0;
var failed = false; var failed = false;
@ -296,6 +300,15 @@ define([
!secret.hashData.present); !secret.hashData.present);
}, "test support for ugly tracking query paramaters in url"); }, "test support for ugly tracking query paramaters in url");
assert(function (cb) {
var keys = Block.genkeys(Nacl.randomBytes(64));
var hash = Block.getBlockHash(keys);
var parsed = Block.parseBlockHash(hash);
cb(parsed &&
parsed.keys.symmetric.length === keys.symmetric.length);
}, 'parse a block hash');
assert(function (cb) { assert(function (cb) {
try { try {
MediaTag(void 0).on('progress').on('decryption'); MediaTag(void 0).on('progress').on('decryption');

@ -134,10 +134,15 @@ define([
}; };
module.exports.load = function (url /*:string*/, cb /*:()=>void*/) { module.exports.load = function (url /*:string*/, cb /*:()=>void*/) {
var btime = +new Date();
var done = function () {
console.log("Compiling [" + url + "] took " + (+new Date() - btime) + "ms");
cb();
};
cacheGet(url, function (css) { cacheGet(url, function (css) {
if (css) { if (css) {
inject(css, url); inject(css, url);
return void cb(); return void done();
} }
console.log('CACHE MISS ' + url); console.log('CACHE MISS ' + url);
((/\.less([\?\#].*)?$/.test(url)) ? loadLess : loadCSS)(url, function (err, css) { ((/\.less([\?\#].*)?$/.test(url)) ? loadLess : loadCSS)(url, function (err, css) {
@ -145,7 +150,7 @@ define([
var output = fixAllURLs(css, url); var output = fixAllURLs(css, url);
cachePut(url, output); cachePut(url, output);
inject(output, url); inject(output, url);
cb(); done();
}); });
}); });
}; };

@ -3,6 +3,7 @@ define(function () {
// localStorage // localStorage
userHashKey: 'User_hash', userHashKey: 'User_hash',
userNameKey: 'User_name', userNameKey: 'User_name',
blockHashKey: 'Block_hash',
fileHashKey: 'FS_hash', fileHashKey: 'FS_hash',
// sessionStorage // sessionStorage
newPadPathKey: "newPadPath", newPadPathKey: "newPadPath",
@ -11,6 +12,7 @@ define(function () {
oldStorageKey: 'CryptPad_RECENTPADS', oldStorageKey: 'CryptPad_RECENTPADS',
storageKey: 'filesData', storageKey: 'filesData',
tokenKey: 'loginToken', tokenKey: 'loginToken',
displayPadCreationScreen: 'displayPadCreationScreen' displayPadCreationScreen: 'displayPadCreationScreen',
deprecatedKey: 'deprecated'
}; };
}); });

@ -147,7 +147,7 @@ define([
id: 'cp-app-prop-expire', id: 'cp-app-prop-expire',
})); }));
var hasPassword = typeof data.password !== "undefined"; var hasPassword = data.password;
if (hasPassword) { if (hasPassword) {
$('<label>', {'for': 'cp-app-prop-password'}).text(Messages.creation_passwordValue) $('<label>', {'for': 'cp-app-prop-password'}).text(Messages.creation_passwordValue)
.appendTo($d); .appendTo($d);
@ -183,23 +183,31 @@ define([
passwordOk passwordOk
]); ]);
$(passwordOk).click(function () { $(passwordOk).click(function () {
var newPass = $(newPassword).find('input').val();
if (data.password === newPass ||
(!data.password && !newPass)) {
return void UI.alert(Messages.properties_passwordSame);
}
UI.confirm(changePwConfirm, function (yes) { UI.confirm(changePwConfirm, function (yes) {
if (!yes) { return; } if (!yes) { return; }
sframeChan.query("Q_PAD_PASSWORD_CHANGE", { sframeChan.query("Q_PAD_PASSWORD_CHANGE", {
href: data.href, href: data.href,
password: $(newPassword).find('input').val() password: newPass
}, function (err, data) { }, function (err, data) {
if (err || data.error) { if (err || data.error) {
return void UI.alert(Messages.properties_passwordError); return void UI.alert(Messages.properties_passwordError);
} }
UI.findOKButton().click(); UI.findOKButton().click();
// If we didn't have a password, we have to add the /p/
// If we had a password and we changed it to a new one, we just have to reload
// If we had a password and we removed it, we have to remove the /p/
if (data.warning) { if (data.warning) {
return void UI.alert(Messages.properties_passwordWarning, function () { return void UI.alert(Messages.properties_passwordWarning, function () {
common.gotoURL(hasPassword ? undefined : data.href); common.gotoURL(hasPassword && newPass ? undefined : data.href);
}, {force: true}); }, {force: true});
} }
return void UI.alert(Messages.properties_passwordSuccess, function () { return void UI.alert(Messages.properties_passwordSuccess, function () {
common.gotoURL(hasPassword ? undefined : data.href); common.gotoURL(hasPassword && newPass ? undefined : data.href);
}, {force: true}); }, {force: true});
}); });
}); });

@ -83,6 +83,21 @@ define([], function () {
}).join(''); }).join('');
}; };
// given an array of Uint8Arrays, return a new Array with all their values
Util.uint8ArrayJoin = function (AA) {
var l = 0;
var i = 0;
for (; i < AA.length; i++) { l += AA[i].length; }
var C = new Uint8Array(l);
i = 0;
for (var offset = 0; i < AA.length; i++) {
C.set(AA[i], offset);
offset += AA[i].length;
}
return C;
};
Util.deduplicateString = function (array) { Util.deduplicateString = function (array) {
var a = array.slice(); var a = array.slice();
for(var i=0; i<a.length; i++) { for(var i=0; i<a.length; i++) {
@ -122,17 +137,14 @@ define([], function () {
else if (bytes >= oneMegabyte) { return 'MB'; } else if (bytes >= oneMegabyte) { return 'MB'; }
}; };
// given a path, asynchronously return an arraybuffer
Util.fetch = function (src, cb) { Util.fetch = function (src, cb) {
var done = false; var CB = Util.once(cb);
var CB = function (err, res) {
if (done) { return; }
done = true;
cb(err, res);
};
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.open("GET", src, true); xhr.open("GET", src, true);
xhr.responseType = "arraybuffer"; xhr.responseType = "arraybuffer";
xhr.onerror = function (err) { CB(err); };
xhr.onload = function () { xhr.onload = function () {
if (/^4/.test(''+this.status)) { if (/^4/.test(''+this.status)) {
return CB('XHR_ERROR'); return CB('XHR_ERROR');

@ -8,11 +8,12 @@ define([
'/common/common-feedback.js', '/common/common-feedback.js',
'/common/outer/local-store.js', '/common/outer/local-store.js',
'/common/outer/worker-channel.js', '/common/outer/worker-channel.js',
'/common/outer/login-block.js',
'/customize/application_config.js', '/customize/application_config.js',
'/bower_components/nthen/index.js', '/bower_components/nthen/index.js',
], function (Config, Messages, Util, Hash, ], function (Config, Messages, Util, Hash,
Messaging, Constants, Feedback, LocalStore, Channel, Messaging, Constants, Feedback, LocalStore, Channel, Block,
AppConfig, Nthen) { AppConfig, Nthen) {
/* This file exposes functionality which is specific to Cryptpad, but not to /* This file exposes functionality which is specific to Cryptpad, but not to
@ -240,6 +241,12 @@ define([
}); });
}; };
common.removeLoginBlock = function (data, cb) {
postMessage('REMOVE_LOGIN_BLOCK', data, function (obj) {
cb(obj);
});
};
// ANON RPC // ANON RPC
// SFRAME: talk to anon_rpc from the iframe // SFRAME: talk to anon_rpc from the iframe
@ -615,10 +622,17 @@ define([
var warning = false; var warning = false;
var newHash; var newHash;
var oldChannel; var oldChannel;
if (parsed.hashData.password) { var newSecret;
newHash = parsed.hash;
if (parsed.hashData.version >= 2) {
newSecret = Hash.getSecrets(parsed.type, parsed.hash, newPassword);
if (!(newSecret.keys && newSecret.keys.editKeyStr)) {
return void cb({error: 'EAUTH'});
}
newHash = Hash.getEditHashFromKeys(newSecret);
} else { } else {
newHash = Hash.createRandomHash(parsed.type, newPassword); newHash = Hash.createRandomHash(parsed.type, newPassword);
newSecret = Hash.getSecrets(parsed.type, newHash, newPassword);
} }
var newHref = '/' + parsed.type + '/#' + newHash; var newHref = '/' + parsed.type + '/#' + newHash;
@ -670,16 +684,17 @@ define([
return void cb(obj); return void cb(obj);
} }
})); }));
common.unpinPads([oldChannel], waitFor());
common.pinPads([newSecret.channel], waitFor());
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
common.setPadAttribute('password', newPassword, waitFor(function (err) { common.setPadAttribute('password', newPassword, waitFor(function (err) {
if (err) { warning = true; } if (err) { warning = true; }
}), href); }), href);
var secret = Hash.getSecrets(parsed.type, newHash, newPassword); common.setPadAttribute('channel', newSecret.channel, waitFor(function (err) {
common.setPadAttribute('channel', secret.channel, waitFor(function (err) {
if (err) { warning = true; } if (err) { warning = true; }
}), href); }), href);
if (parsed.hashData.password) { return; } // same hash if (parsed.hashData.password && newPassword) { return; } // same hash
common.setPadAttribute('href', newHref, waitFor(function (err) { common.setPadAttribute('href', newHref, waitFor(function (err) {
if (err) { warning = true; } if (err) { warning = true; }
}), href); }), href);
@ -692,6 +707,180 @@ define([
}); });
}; };
common.changeUserPassword = function (Crypt, edPublic, data, cb) {
if (!edPublic) {
return void cb({
error: 'E_NOT_LOGGED_IN'
});
}
var accountName = LocalStore.getAccountName();
var hash = LocalStore.getUserHash();
if (!hash) {
return void cb({
error: 'E_NOT_LOGGED_IN'
});
}
var password = data.password; // To remove your old block
var newPassword = data.newPassword; // To create your new block
var secret = Hash.getSecrets('drive', hash);
var newHash, newHref, newSecret, blockKeys;
var oldIsOwned = false;
var blockHash = LocalStore.getBlockHash();
var oldBlockKeys;
var Cred, Block, Login;
Nthen(function (waitFor) {
require([
'/customize/credential.js',
'/common/outer/login-block.js',
'/customize/login.js'
], waitFor(function (_Cred, _Block, _Login) {
Cred = _Cred;
Block = _Block;
Login = _Login;
}));
}).nThen(function (waitFor) {
// confirm that the provided password is correct
Cred.deriveFromPassphrase(accountName, password, Login.requiredBytes, waitFor(function (bytes) {
var allocated = Login.allocateBytes(bytes);
oldBlockKeys = allocated.blockKeys;
if (blockHash) {
if (blockHash !== allocated.blockHash) {
console.log("provided password did not yield the correct blockHash");
// incorrect password probably
waitFor.abort();
return void cb({
error: 'INVALID_PASSWORD',
});
}
// the user has already created a block, so you should compare against that
} else {
// otherwise they're a legacy user, and we should check against the User_hash
if (hash !== allocated.userHash) {
console.log("provided password did not yield the correct userHash");
waitFor.abort();
return void cb({
error: 'INVALID_PASSWORD',
});
}
}
}));
}).nThen(function (waitFor) {
// Check if our drive is already owned
console.log("checking if old drive is owned");
common.anonRpcMsg('GET_METADATA', secret.channel, waitFor(function (err, obj) {
if (err || obj.error) { return; }
if (obj.owners && Array.isArray(obj.owners) &&
obj.owners.indexOf(edPublic) !== -1) {
oldIsOwned = true;
}
}));
}).nThen(function (waitFor) {
// Create a new user hash
// Get the current content, store it in the new user file
// and make sure the new user drive is owned
newHash = Hash.createRandomHash('drive');
newHref = '/drive/#' + newHash;
newSecret = Hash.getSecrets('drive', newHash);
var optsPut = {
owners: [edPublic],
initialState: '{}',
};
console.log("copying contents of old drive to new location");
Crypt.get(hash, waitFor(function (err, val) {
if (err) {
waitFor.abort();
return void cb({ error: err });
}
Crypt.put(newHash, val, waitFor(function (err) {
if (err) {
waitFor.abort();
console.error(err);
return void cb({ error: err });
}
}), optsPut);
}));
}).nThen(function (waitFor) {
// Drive content copied: get the new block location
console.log("deriving new credentials from passphrase");
Cred.deriveFromPassphrase(accountName, newPassword, Login.requiredBytes, waitFor(function (bytes) {
var allocated = Login.allocateBytes(bytes);
blockKeys = allocated.blockKeys;
}));
}).nThen(function (waitFor) {
// Write the new login block
var temp = {
User_name: accountName,
User_hash: newHash,
edPublic: edPublic,
};
var content = Block.serialize(JSON.stringify(temp), blockKeys);
console.log("writing new login block");
common.writeLoginBlock(content, waitFor(function (obj) {
if (obj && obj.error) {
waitFor.abort();
return void cb(obj);
}
console.log("new login block written");
var newBlockHash = Block.getBlockHash(blockKeys);
LocalStore.setBlockHash(newBlockHash);
}));
}).nThen(function (waitFor) {
// New drive hash is in login block, unpin the old one and pin the new one
console.log("unpinning old drive and pinning new one");
common.unpinPads([secret.channel], waitFor());
common.pinPads([newSecret.channel], waitFor());
}).nThen(function (waitFor) {
// Remove block hash
if (blockHash) {
console.log('removing old login block');
var removeData = Block.remove(oldBlockKeys);
common.removeLoginBlock(removeData, waitFor(function (obj) {
if (obj && obj.error) { return void console.error(obj.error); }
}));
}
}).nThen(function (waitFor) {
if (oldIsOwned) {
console.log('removing old drive');
common.removeOwnedChannel(secret.channel, waitFor(function (obj) {
if (obj && obj.error) {
// Deal with it as if it was not owned
oldIsOwned = false;
return;
}
common.logoutFromAll(waitFor(function () {
postMessage("DISCONNECT");
}));
}));
}
}).nThen(function (waitFor) {
if (!oldIsOwned) {
console.error('deprecating old drive.');
postMessage("SET", {
key: [Constants.deprecatedKey],
value: true
}, waitFor(function (obj) {
if (obj && obj.error) {
console.error(obj.error);
}
common.logoutFromAll(waitFor(function () {
postMessage("DISCONNECT");
}));
}));
}
}).nThen(function () {
// We have the new drive, with the new login block
window.location.reload();
});
};
// Loading events // Loading events
common.loading = {}; common.loading = {};
common.loading.onDriveEvent = Util.mkEvent(); common.loading.onDriveEvent = Util.mkEvent();
@ -834,6 +1023,20 @@ define([
LOADING_DRIVE: common.loading.onDriveEvent.fire LOADING_DRIVE: common.loading.onDriveEvent.fire
}; };
common.hasCSSVariables = function () {
if (window.CSS && window.CSS.supports && window.CSS.supports('--a', 0)) { return true; }
// Safari lol y u always b returnin false ?
var color = 'rgb(255, 198, 0)';
var el = document.createElement('span');
el.style.setProperty('--color', color);
el.style.setProperty('background', 'var(--color)');
document.body.appendChild(el);
var styles = getComputedStyle(el);
var doesSupport = (styles.backgroundColor === color);
document.body.removeChild(el);
return doesSupport;
};
common.ready = (function () { common.ready = (function () {
var env = {}; var env = {};
var initialized = false; var initialized = false;
@ -870,9 +1073,12 @@ define([
if (typeof(Worker) === "undefined") { if (typeof(Worker) === "undefined") {
Feedback.send('NO_WEBWORKER'); Feedback.send('NO_WEBWORKER');
} }
if (typeof(ServiceWorker) === "undefined") { if (!('serviceWorker' in navigator)) {
Feedback.send('NO_SERVICEWORKER'); Feedback.send('NO_SERVICEWORKER');
} }
if (!common.hasCSSVariables()) {
Feedback.send('NO_CSS_VARIABLES');
}
Feedback.reportScreenDimensions(); Feedback.reportScreenDimensions();
Feedback.reportLanguage(); Feedback.reportLanguage();
@ -883,16 +1089,53 @@ define([
provideFeedback(); provideFeedback();
}; };
var userHash;
Nthen(function (waitFor) { Nthen(function (waitFor) {
if (AppConfig.beforeLogin) { if (AppConfig.beforeLogin) {
AppConfig.beforeLogin(LocalStore.isLoggedIn(), waitFor()); AppConfig.beforeLogin(LocalStore.isLoggedIn(), waitFor());
} }
}).nThen(function (waitFor) {
var blockHash = LocalStore.getBlockHash();
if (blockHash) {
console.log(blockHash);
var parsed = Block.parseBlockHash(blockHash);
if (typeof(parsed) !== 'object') {
console.error("Failed to parse blockHash");
console.log(parsed);
return;
} else {
console.log(parsed);
}
Util.fetch(parsed.href, waitFor(function (err, arraybuffer) {
if (err) { return void console.log(err); }
// use the results to load your user hash and
// put your userhash into localStorage
try {
var block_info = Block.decrypt(arraybuffer, parsed.keys);
if (!block_info) {
console.error("Failed to decrypt !");
return;
}
userHash = block_info[Constants.userHashKey];
if (!userHash || userHash !== LocalStore.getUserHash()) {
return void requestLogin();
}
} catch (e) {
console.error(e);
return void console.error("failed to decrypt or decode block content");
}
}));
}
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
var cfg = { var cfg = {
init: true, init: true,
userHash: LocalStore.getUserHash(), userHash: userHash || LocalStore.getUserHash(),
anonHash: LocalStore.getFSHash(), anonHash: LocalStore.getFSHash(),
localToken: tryParsing(localStorage.getItem(Constants.tokenKey)), localToken: tryParsing(localStorage.getItem(Constants.tokenKey)), // TODO move this to LocalStore ?
language: common.getLanguage(), language: common.getLanguage(),
messenger: rdyCfg.messenger, // Boolean messenger: rdyCfg.messenger, // Boolean
driveEvents: rdyCfg.driveEvents // Boolean driveEvents: rdyCfg.driveEvents // Boolean
@ -915,18 +1158,23 @@ define([
Nthen(function (waitFor2) { Nthen(function (waitFor2) {
if (Worker) { if (Worker) {
var w = waitFor2(); var w = waitFor2();
worker = new Worker('/common/outer/testworker.js?' + urlArgs); try {
worker.onerror = function (errEv) { worker = new Worker('/common/outer/testworker.js?' + urlArgs);
errEv.preventDefault(); worker.onerror = function (errEv) {
errEv.stopPropagation(); errEv.preventDefault();
errEv.stopPropagation();
noWorker = true;
w();
};
worker.onmessage = function (ev) {
if (ev.data === "OK") {
w();
}
};
} catch (e) {
noWorker = true; noWorker = true;
w(); w();
}; }
worker.onmessage = function (ev) {
if (ev.data === "OK") {
w();
}
};
} }
if (typeof(SharedWorker) !== "undefined") { if (typeof(SharedWorker) !== "undefined") {
try { try {

@ -13,7 +13,7 @@ define([
'/common/outer/network-config.js', '/common/outer/network-config.js',
'/customize/application_config.js', '/customize/application_config.js',
'/bower_components/chainpad-crypto/crypto.js?v=0.1.5', '/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad/chainpad.dist.js', '/bower_components/chainpad/chainpad.dist.js',
'/bower_components/chainpad-listmap/chainpad-listmap.js', '/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/nthen/index.js', '/bower_components/nthen/index.js',
@ -285,6 +285,15 @@ define([
}); });
}; };
Store.removeLoginBlock = function (clientId, data, cb) {
store.rpc.removeLoginBlock(data, function (e, res) {
cb({
error: e,
data: res
});
});
};
Store.initRpc = function (clientId, data, cb) { Store.initRpc = function (clientId, data, cb) {
if (store.rpc) { return void cb(account); } if (store.rpc) { return void cb(account); }
require(['/common/pinpad.js'], function (Pinpad) { require(['/common/pinpad.js'], function (Pinpad) {

@ -58,6 +58,14 @@ define([
localStorage[Constants.userHashKey] = sHash; localStorage[Constants.userHashKey] = sHash;
}; };
LocalStore.getBlockHash = function () {
return localStorage[Constants.blockHashKey];
};
LocalStore.setBlockHash = function (hash) {
localStorage[Constants.blockHashKey] = hash;
};
LocalStore.getAccountName = function () { LocalStore.getAccountName = function () {
return localStorage[Constants.userNameKey]; return localStorage[Constants.userNameKey];
}; };
@ -66,10 +74,6 @@ define([
return typeof getUserHash() === "string"; return typeof getUserHash() === "string";
}; };
LocalStore.login = function (hash, name, cb) { LocalStore.login = function (hash, name, cb) {
if (!hash) { throw new Error('expected a user hash'); } if (!hash) { throw new Error('expected a user hash'); }
if (!name) { throw new Error('expected a user name'); } if (!name) { throw new Error('expected a user name'); }
@ -96,6 +100,7 @@ define([
[ [
Constants.userNameKey, Constants.userNameKey,
Constants.userHashKey, Constants.userHashKey,
Constants.blockHashKey,
'loginToken', 'loginToken',
'plan', 'plan',
].forEach(function (k) { ].forEach(function (k) {

@ -0,0 +1,147 @@
define([
'/common/common-util.js',
'/api/config',
'/bower_components/tweetnacl/nacl-fast.min.js',
], function (Util, ApiConfig) {
var Nacl = window.nacl;
var Block = {};
Block.join = Util.uint8ArrayJoin;
// publickey <base64 string>
// signature <base64 string>
// block <base64 string>
// [b64_public, b64_sig, b64_block [version, nonce, content]]
Block.seed = function () {
return Nacl.hash(Nacl.util.decodeUTF8('pewpewpew'));
};
// should be deterministic from a seed...
Block.genkeys = function (seed) {
if (!(seed instanceof Uint8Array)) {
throw new Error('INVALID_SEED_FORMAT');
}
if (!seed || typeof(seed.length) !== 'number' || seed.length < 64) {
throw new Error('INVALID_SEED_LENGTH');
}
var signSeed = seed.subarray(0, Nacl.sign.seedLength);
var symmetric = seed.subarray(Nacl.sign.seedLength,
Nacl.sign.seedLength + Nacl.secretbox.keyLength);
return {
sign: Nacl.sign.keyPair.fromSeed(signSeed), // 32 bytes
symmetric: symmetric, // 32 bytes ...
};
};
// (UTF8 content, keys object) => Uint8Array block
Block.encrypt = function (version, content, keys) {
var u8 = Nacl.util.decodeUTF8(content);
var nonce = Nacl.randomBytes(Nacl.secretbox.nonceLength);
return Block.join([
[0],
nonce,
Nacl.secretbox(u8, nonce, keys.symmetric)
]);
};
// (uint8Array block) => payload object
Block.decrypt = function (u8_content, keys) {
// version is currently ignored since there is only one
var nonce = u8_content.subarray(1, 1 + Nacl.secretbox.nonceLength);
var box = u8_content.subarray(1 + Nacl.secretbox.nonceLength);
var plaintext = Nacl.secretbox.open(box, nonce, keys.symmetric);
try {
return JSON.parse(Nacl.util.encodeUTF8(plaintext));
} catch (e) {
console.error(e);
return;
}
};
// (Uint8Array block) => signature
Block.sign = function (ciphertext, keys) {
return Nacl.sign.detached(Nacl.hash(ciphertext), keys.sign.secretKey);
};
Block.serialize = function (content, keys) {
// encrypt the content
var ciphertext = Block.encrypt(0, content, keys);
// generate a detached signature
var sig = Block.sign(ciphertext, keys);
// serialize {publickey, sig, ciphertext}
return {
publicKey: Nacl.util.encodeBase64(keys.sign.publicKey),
signature: Nacl.util.encodeBase64(sig),
ciphertext: Nacl.util.encodeBase64(ciphertext),
};
};
Block.remove = function (keys) {
// sign the hash of the text 'DELETE_BLOCK'
var sig = Nacl.sign.detached(Nacl.hash(
Nacl.util.decodeUTF8('DELETE_BLOCK')), keys.sign.secretKey);
return {
publicKey: Nacl.util.encodeBase64(keys.sign.publicKey),
signature: Nacl.util.encodeBase64(sig),
};
};
var urlSafeB64 = function (u8) {
return Nacl.util.encodeBase64(u8).replace(/\//g, '-');
};
Block.getBlockUrl = function (keys) {
var publicKey = urlSafeB64(keys.sign.publicKey);
// 'block/' here is hardcoded because it's hardcoded on the server
// if we want to make CryptPad work in server subfolders, we'll need
// to update this path derivation
return (ApiConfig.fileHost || window.location.origin)
+ '/block/' + publicKey.slice(0, 2) + '/' + publicKey;
};
Block.getBlockHash = function (keys) {
var absolute = Block.getBlockUrl(keys);
var symmetric = urlSafeB64(keys.symmetric);
return absolute + '#' + symmetric;
};
var decodeSafeB64 = function (b64) {
try {
return Nacl.util.decodeBase64(b64.replace(/\-/g, '/'));
} catch (e) {
console.error(e);
return;
}
};
Block.parseBlockHash = function (hash) {
if (typeof(hash) !== 'string') { return; }
var parts = hash.split('#');
if (parts.length !== 2) { return; }
try {
return {
href: parts[0],
keys: {
symmetric: decodeSafeB64(parts[1]),
}
};
} catch (e) {
console.error(e);
return;
}
};
return Block;
});

@ -24,6 +24,7 @@ define([
UPLOAD_STATUS: Store.uploadStatus, UPLOAD_STATUS: Store.uploadStatus,
UPLOAD_CANCEL: Store.uploadCancel, UPLOAD_CANCEL: Store.uploadCancel,
WRITE_LOGIN_BLOCK: Store.writeLoginBlock, WRITE_LOGIN_BLOCK: Store.writeLoginBlock,
REMOVE_LOGIN_BLOCK: Store.removeLoginBlock,
PIN_PADS: Store.pinPads, PIN_PADS: Store.pinPads,
UNPIN_PADS: Store.unpinPads, UNPIN_PADS: Store.unpinPads,
GET_DELETED_PADS: Store.getDeletedPads, GET_DELETED_PADS: Store.getDeletedPads,

@ -563,34 +563,41 @@ define([
continue; continue;
} }
// Clean missing href // Clean missing href
if (!el.href) { var parsed;
debug("Removing an element in filesData with a missing href.", el); if (el.href) {
toClean.push(id); if (!el.href) {
continue; debug("Removing an element in filesData with a missing href.", el);
} toClean.push(id);
continue;
}
var parsed = Hash.parsePadUrl(el.href); parsed = Hash.parsePadUrl(el.href);
// Clean invalid hash // Clean invalid hash
if (!parsed.hash) { if (!parsed.hash) {
debug("Removing an element in filesData with a invalid href.", el); debug("Removing an element in filesData with a invalid href.", el);
toClean.push(id); toClean.push(id);
continue; continue;
} }
// Clean invalid type // Clean invalid type
if (!parsed.type) { if (!parsed.type) {
debug("Removing an element in filesData with a invalid type.", el); debug("Removing an element in filesData with a invalid type.", el);
toClean.push(id);
continue;
}
} else if (!el.roHref) {
debug("Removing an element in filesData with a missing href.", el);
toClean.push(id); toClean.push(id);
continue; continue;
} }
// Fix href // Fix href
if (/^https*:\/\//.test(el.href)) { el.href = Hash.getRelativeHref(el.href); } if (el.href && /^https*:\/\//.test(el.href)) { el.href = Hash.getRelativeHref(el.href); }
// Fix creation time // Fix creation time
if (!el.ctime) { el.ctime = el.atime; } if (!el.ctime) { el.ctime = el.atime; }
// Fix title // Fix title
if (!el.title) { el.title = Hash.getDefaultName(parsed); } if (!el.title) { el.title = Hash.getDefaultName(parsed); }
// Fix channel // Fix channel
if (!el.channel) { if (el.href && !el.channel) {
try { try {
var secret = Hash.getSecrets(parsed.type, parsed.hash, el.password); var secret = Hash.getSecrets(parsed.type, parsed.hash, el.password);
el.channel = secret.channel; el.channel = secret.channel;

@ -222,7 +222,34 @@ define([
}; };
exp.writeLoginBlock = function (data, cb) { exp.writeLoginBlock = function (data, cb) {
cb(); if (!data) { return void cb('NO_DATA'); }
if (!data.publicKey || !data.signature || !data.ciphertext) {
console.log(data);
return void cb("MISSING_PARAMETERS");
}
rpc.send('WRITE_LOGIN_BLOCK', [
data.publicKey,
data.signature,
data.ciphertext
], function (e) {
cb(e);
});
};
exp.removeLoginBlock = function (data, cb) {
if (!data) { return void cb('NO_DATA'); }
if (!data.publicKey || !data.signature) {
console.log(data);
return void cb("MISSING_PARAMETERS");
}
rpc.send('REMOVE_LOGIN_BLOCK', [
data.publicKey, // publicKey
data.signature, // signature
], function (e) {
cb(e);
});
}; };
cb(e, exp); cb(e, exp);

@ -126,7 +126,7 @@ define([
if (newState === STATE.INFINITE_SPINNER || newState === STATE.DELETED) { if (newState === STATE.INFINITE_SPINNER || newState === STATE.DELETED) {
state = newState; state = newState;
} else if (state === STATE.DISCONNECTED && newState !== STATE.INITIALIZING) { } else if (state === STATE.DISCONNECTED && newState !== STATE.INITIALIZING) {
throw new Error("Cannot transition from DISCONNECTED to " + newState); throw new Error("Cannot transition from DISCONNECTED to " + newState); // FIXME we are getting "DISCONNECTED to READY" on prod
} else if (state !== STATE.READY && newState === STATE.HISTORY_MODE) { } else if (state !== STATE.READY && newState === STATE.HISTORY_MODE) {
throw new Error("Cannot transition from " + state + " to " + newState); throw new Error("Cannot transition from " + state + " to " + newState);
} else { } else {

@ -661,10 +661,18 @@ define([
Cryptpad.changePadPassword(Cryptget, href, data.password, edPublic, cb); Cryptpad.changePadPassword(Cryptget, href, data.password, edPublic, cb);
}); });
sframeChan.on('Q_CHANGE_USER_PASSWORD', function (data, cb) {
Cryptpad.changeUserPassword(Cryptget, edPublic, data, cb);
});
sframeChan.on('Q_WRITE_LOGIN_BLOCK', function (data, cb) { sframeChan.on('Q_WRITE_LOGIN_BLOCK', function (data, cb) {
Cryptpad.writeLoginBlock(data, cb); Cryptpad.writeLoginBlock(data, cb);
}); });
sframeChan.on('Q_REMOVE_LOGIN_BLOCK', function (data, cb) {
Cryptpad.removeLoginBlock(data, cb);
});
if (cfg.addRpc) { if (cfg.addRpc) {
cfg.addRpc(sframeChan, Cryptpad, Utils); cfg.addRpc(sframeChan, Cryptpad, Utils);
} }

@ -77,6 +77,9 @@ define({
// Write/update the login block when the account password is changed // Write/update the login block when the account password is changed
'Q_WRITE_LOGIN_BLOCK': true, 'Q_WRITE_LOGIN_BLOCK': true,
// Remove login blocks
'Q_REMOVE_LOGIN_BLOCK': true,
// Check the pin limit to determine if we can store the pad in the drive or if we should. // Check the pin limit to determine if we can store the pad in the drive or if we should.
// display a warning // display a warning
'Q_GET_PIN_LIMIT_STATUS': true, 'Q_GET_PIN_LIMIT_STATUS': true,
@ -235,6 +238,9 @@ define({
// Change pad password // Change pad password
'Q_PAD_PASSWORD_CHANGE': true, 'Q_PAD_PASSWORD_CHANGE': true,
// Migrate drive to owned drive
'Q_CHANGE_USER_PASSWORD': true,
// Loading events to display in the loading screen // Loading events to display in the loading screen
'EV_LOADING_INFO': true, 'EV_LOADING_INFO': true,
// Critical error outside the iframe during loading screen // Critical error outside the iframe during loading screen

@ -37,7 +37,14 @@ define([
var logError = config.logError || logging; var logError = config.logError || logging;
var debug = exp.debug = config.debug || logging; var debug = exp.debug = config.debug || logging;
var error = exp.error = function() { var error = exp.error = function() {
exp.fixFiles(); if (sframeChan) {
return void sframeChan.query("Q_DRIVE_USEROBJECT", {
cmd: "fixFiles",
data: {}
}, function () {});
} else if (typeof (exp.fixFiles) === "function") {
exp.fixFiles();
}
console.error.apply(console, arguments); console.error.apply(console, arguments);
}; };

@ -1367,6 +1367,10 @@ define([
} }
var element = filesOp.find(newPath); var element = filesOp.find(newPath);
if (!isFolder) {
var data = filesOp.getFileData(element);
if (!data.href) { return; }
}
var $icon = !isFolder ? getFileIcon(element) : undefined; var $icon = !isFolder ? getFileIcon(element) : undefined;
var ro = filesOp.isReadOnlyFile(element); var ro = filesOp.isReadOnlyFile(element);
// ro undefined means it's an old hash which doesn't support read-only // ro undefined means it's an old hash which doesn't support read-only
@ -2424,6 +2428,7 @@ define([
sortedFiles.forEach(function (key) { sortedFiles.forEach(function (key) {
if (filesOp.isFolder(root[key])) { return; } if (filesOp.isFolder(root[key])) { return; }
var $element = createElement(path, key, root, false); var $element = createElement(path, key, root, false);
if (!$element) { return; }
$element.appendTo($list); $element.appendTo($list);
}); });

@ -326,7 +326,21 @@ define([
if (framework.isReadOnly()) { if (framework.isReadOnly()) {
$container.addClass('cp-app-readonly'); $container.addClass('cp-app-readonly');
} else {
framework.setFileImporter({}, function (content /*, file */) {
var parsed;
try { parsed = JSON.parse(content); }
catch (e) { return void console.error(e); }
return { content: parsed };
});
} }
framework.setFileExporter('json', function () {
return new Blob([JSON.stringify(kanban.getBoardsJSON())], {
type: 'application/json',
});
});
framework.onEditableChange(function (unlocked) { framework.onEditableChange(function (unlocked) {
if (framework.isReadOnly()) { return; } if (framework.isReadOnly()) { return; }
if (!kanban) { return; } if (!kanban) { return; }

@ -57,11 +57,6 @@ define([
var test; var test;
$register.click(function () { $register.click(function () {
if (registering) {
console.log("registration is already in progress");
return;
}
var uname = $uname.val(); var uname = $uname.val();
var passwd = $passwd.val(); var passwd = $passwd.val();
var confirmPassword = $confirm.val(); var confirmPassword = $confirm.val();

@ -50,7 +50,7 @@ define([
'cp-settings-resettips', 'cp-settings-resettips',
'cp-settings-thumbnails', 'cp-settings-thumbnails',
'cp-settings-userfeedback', 'cp-settings-userfeedback',
//'cp-settings-change-password', 'cp-settings-change-password',
'cp-settings-delete' 'cp-settings-delete'
], ],
'creation': [ 'creation': [
@ -404,12 +404,11 @@ define([
$(form).appendTo($div); $(form).appendTo($div);
var updateBlock = function (data, cb) { var updateBlock = function (data, cb) {
sframeChan.query('Q_WRITE_LOGIN_BLOCK', data, function (err, obj) { sframeChan.query('Q_CHANGE_USER_PASSWORD', data, function (err, obj) {
if (err || obj.error) { return void cb ({error: err || obj.error}); } if (err || obj.error) { return void cb ({error: err || obj.error}); }
cb (obj); cb (obj);
}); });
}; };
updateBlock = updateBlock; // jshint..
var todo = function () { var todo = function () {
var oldPassword = $(form).find('#cp-settings-change-password-current').val(); var oldPassword = $(form).find('#cp-settings-change-password-current').val();
@ -432,8 +431,21 @@ define([
UI.confirm(Messages.settings_changePasswordConfirm, UI.confirm(Messages.settings_changePasswordConfirm,
function (yes) { function (yes) {
if (!yes) { return; } if (!yes) { return; }
// TODO
console.log(oldPassword, newPassword, newPasswordConfirm); UI.addLoadingScreen({
hideTips: true,
loadingText: Messages.settings_changePasswordPending,
});
updateBlock({
password: oldPassword,
newPassword: newPassword
}, function (obj) {
UI.removeLoadingScreen();
if (obj && obj.error) {
// TODO
UI.alert(Messages.settings_changePasswordError);
}
});
}, { }, {
ok: Messages.register_writtenPassword, ok: Messages.register_writtenPassword,
cancel: Messages.register_cancel, cancel: Messages.register_cancel,
@ -461,6 +473,50 @@ define([
return $div; return $div;
}; };
create['migrate'] = function () {
if (true) { return; } // STUBBED until we have a reason to deploy this
// TODO
// if (!loginBlock) { return; }
// if (alreadyMigrated) { return; }
if (!common.isLoggedIn()) { return; }
var $div = $('<div>', { 'class': 'cp-settings-migrate cp-sidebarlayout-element'});
$('<span>', {'class': 'label'}).text(Messages.settings_ownDriveTitle).appendTo($div);
$('<span>', {'class': 'cp-sidebarlayout-description'})
.append(Messages.settings_ownDriveHint).appendTo($div);
var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved});
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'});
var $button = $('<button>', {'id': 'cp-settings-delete', 'class': 'btn btn-primary'})
.text(Messages.settings_ownDriveButton).appendTo($div);
$button.click(function () {
$spinner.show();
UI.confirm(Messages.settings_ownDriveConfirm, function (yes) {
if (!yes) { return; }
sframeChan.query("Q_OWN_USER_DRIVE", null, function (err, data) {
if (err || data.error) {
console.error(err || data.error);
// TODO
$spinner.hide();
return;
}
// TODO: drive is migrated, autoamtic redirect from outer?
$ok.show();
$spinner.hide();
});
});
});
$spinner.hide().appendTo($div);
$ok.hide().appendTo($div);
return $div;
};
// Pad Creation settings // Pad Creation settings
var setHTML = function (e, html) { var setHTML = function (e, html) {

@ -333,15 +333,6 @@ define([
framework.localChange(); framework.localChange();
}; };
// Export to drive as PNG
framework._.sfCommon.createButton('savetodrive', true, {}).click(function () {
var defaultName = framework._.title.getTitle();
UI.prompt(Messages.exportPrompt, defaultName + '.png', function (name) {
if (name === null || !name.trim()) { return; }
APP.upload(name);
});
}).appendTo($rightside);
// Embed image // Embed image
var onUpload = function (e) { var onUpload = function (e) {
var file = e.target.files[0]; var file = e.target.files[0];
@ -390,6 +381,15 @@ define([
}; };
framework._.sfCommon.openFilePicker(pickerCfg); framework._.sfCommon.openFilePicker(pickerCfg);
}).appendTo($rightside); }).appendTo($rightside);
// Export to drive as PNG
framework._.sfCommon.createButton('savetodrive', true, {}).click(function () {
var defaultName = framework._.title.getTitle();
UI.prompt(Messages.exportPrompt, defaultName + '.png', function (name) {
if (name === null || !name.trim()) { return; }
APP.upload(name);
});
}).appendTo($rightside);
} }
if (framework.isReadOnly()) { if (framework.isReadOnly()) {

Loading…
Cancel
Save