Merge branch 'master' into revised-german

This commit is contained in:
Paul Libbrecht 2018-08-03 18:01:16 +02:00 committed by GitHub
commit 365d17e275
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1227 additions and 155 deletions

1
.gitignore vendored
View file

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

View file

@ -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)
## Goals

View file

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

25
Dockerfile.arm64 Normal file
View file

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

View file

@ -211,6 +211,11 @@ module.exports = {
*/
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
* the people who have accounts. This check-in will also send the version of your CryptPad

View file

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

View file

@ -12,17 +12,23 @@ define([
'/common/common-feedback.js',
'/common/outer/local-store.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/scrypt-async/scrypt-async.min.js', // better load speed
], function ($, Listmap, Crypto, Util, NetConfig, Cred, ChainPad, Realtime, Constants, UI,
Feedback, LocalStore, Messages) {
Feedback, LocalStore, Messages, nThen, Block, Hash) {
var Exports = {
Cred: Cred,
// this is depended on by non-customizable files
// be careful when modifying login.js
requiredBytes: 192,
};
var Nacl = window.nacl;
var allocateBytes = function (bytes) {
var allocateBytes = Exports.allocateBytes = function (bytes) {
var dispense = Cred.dispenser(bytes);
var opt = {};
@ -41,6 +47,10 @@ define([
// 32 more for a signing key
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
var signingKeypair = Nacl.sign.keyPair.fromSeed(new Uint8Array(edSeed));
@ -58,13 +68,27 @@ define([
// should never happen
if (channelHex.length !== 32) { throw new Error('invalid channel id'); }
opt.channel64 = Util.hexToBase64(channelHex);
var channel64 = Util.hexToBase64(channelHex);
opt.userHash = '/1/edit/' + [opt.channel64, opt.keys.editKeyStr].join('/');
// 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;
};
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;
};
var loadUserObject = function (opt, cb) {
var config = {
websocketURL: NetConfig.getWebsocketURL(),
@ -92,6 +116,14 @@ define([
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) {
if (typeof(cb) !== 'function') { return; }
@ -105,22 +137,101 @@ define([
return void cb('PASS_TOO_SHORT');
}
Cred.deriveFromPassphrase(uname, passwd, 128, function (bytes) {
// results...
var res = {
register: isRegister,
};
// results...
var res = {
register: isRegister,
};
// run scrypt to derive the user's keys
var opt = res.opt = allocateBytes(bytes);
var RT, blockKeys, blockHash, Pinpad, rpc, userHash;
// use the derived key to generate an object
loadUserObject(opt, function (err, rt) {
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;
}
console.error(decryptedBlock);
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; }
var opt = res.opt;
// load the user's object using the legacy credentials
loadUserObject(opt, waitFor(function (err, rt) {
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.realtime = rt.realtime;
res.network = rt.network;
// they're registering...
res.userHash = opt.userHash;
@ -130,39 +241,14 @@ define([
res.edPrivate = opt.edPrivate;
res.edPublic = opt.edPublic;
// export their encryption key
res.curvePrivate = opt.curvePrivate;
res.curvePublic = opt.curvePublic;
// they tried to just log in but there's no such user
if (!isRegister && isProxyEmpty(rt.proxy)) {
rt.network.disconnect(); // clean up after yourself
return void cb('NO_SUCH_USER', res);
}
if (shouldImport) { setMergeAnonDrive(); }
// they tried to register, but those exact credentials exist
if (isRegister && !isProxyEmpty(rt.proxy)) {
rt.network.disconnect();
return void cb('ALREADY_REGISTERED', res);
}
if (isRegister) {
var proxy = rt.proxy;
proxy.edPublic = res.edPublic;
proxy.edPrivate = res.edPrivate;
proxy.curvePublic = res.curvePublic;
proxy.curvePrivate = res.curvePrivate;
proxy.login_name = uname;
proxy[Constants.displayNameKey] = uname;
sessionStorage.createReadme = 1;
if (!shouldImport) { proxy.version = 6; }
Feedback.send('REGISTRATION', true);
} else {
Feedback.send('LOGIN', true);
}
if (shouldImport) {
sessionStorage.migrateAnonDrive = 1;
}
// 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)`
@ -171,12 +257,152 @@ define([
// `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)) {
// this really shouldn't happen, but let's handle it anyway
Feedback.send('EMPTY_LOGIN_WITH_BLOCK');
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();
res.blockHash = blockHash;
if (shouldImport) {
setMergeAnonDrive();
}
return void cb('ALREADY_REGISTERED', res);
}
if (!isRegister && !isProxyEmpty(rt.proxy)) {
LocalStore.setBlockHash(blockHash);
waitFor.abort();
if (shouldImport) {
setMergeAnonDrive();
}
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[Constants.displayNameKey] = uname;
setCreateReadme();
if (shouldImport) {
setMergeAnonDrive();
} else {
proxy.version = 6;
}
Feedback.send('REGISTRATION', true);
} else {
Feedback.send('LOGIN', true);
}
setTimeout(waitFor(function () {
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");
// Finally, create the login block for the object you just created.
var toPublish = {};
toPublish[Constants.userNameKey] = uname;
toPublish[Constants.userHashKey] = userHash;
toPublish.edPublic = RT.proxy.edPublic;
var blockRequest = Block.serialize(JSON.stringify(toPublish), res.opt.blockKeys);
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 () {
@ -255,7 +481,6 @@ define([
});
break;
case 'ALREADY_REGISTERED':
// logMeIn should reset registering = false
UI.removeLoadingScreen(function () {
UI.confirm(Messages.register_alreadyRegistered, function (yes) {
if (!yes) {
@ -268,6 +493,12 @@ define([
proxy[Constants.displayNameKey] = uname;
}
LocalStore.eraseTempSessionValues();
if (result.blockHash) {
LocalStore.setBlockHash(result.blockHash);
}
LocalStore.login(result.userHash, result.userName, function () {
setTimeout(function () { proceed(result); });
});

View file

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

View file

@ -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.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.deletedError = 'Dieses Dokument wurde von seinem Besitzer gelöscht und ist 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.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>'+
'Drücke auf <em>Esc</em>, um das Dokument schreibgeschützt zu lesen, oder lade es neu, um das Editierien wieder aufzunehmen.';
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.errorRedirectToHome = 'Drückee <em>Esc</em> um zu Deinem CryptDrive zurückzukehren.';
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.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. Drucke auf die Esc-Taste, um ein neues Dokument zu gestalten.';
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>'+
'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 kopieren, nachdem du <em>Esc</em> drucken.<br>Wenn du die Seite verlässt, verschwindet der Inhalt für immer!';
out.errorRedirectToHome = 'Drucke <em>Esc</em>, um zu deinem CryptDrive zu gehen.';
out.loading = "Laden...";
out.error = "Fehler";
out.saved = "Gespeichert";

View file

@ -49,7 +49,7 @@ define(function () {
out.deleted = "Pad supprimé de votre CryptDrive";
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.synchronizing = 'Synchronisation';
@ -235,7 +235,6 @@ define(function () {
out.history_next = "Version plus récente";
out.history_prev = "Version plus ancienne";
out.history_loadMore = "Charger davantage d'historique";
out.history_close = "Retour";
out.history_closeTitle = "Fermer l'historique";
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 ?";
@ -597,6 +596,17 @@ define(function () {
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_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_modal_title = "Options d'importation du fichier";
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_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_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.";
// 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>" +
"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 = {
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>" +
"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>" +
"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: {
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_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_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_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.";

View file

@ -50,7 +50,7 @@ define(function () {
out.deleted = "Pad deleted from your CryptDrive";
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.synchronizing = 'Synchronizing';
@ -600,14 +600,21 @@ define(function () {
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_changePasswordTitle = "Change your password"; // 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>" +
"<b>We can't reset your password if you forget it so be very careful!</b>"; // XXX
out.settings_changePasswordButton = "Change password"; // XXX
out.settings_changePasswordCurrent = "Existing password"; // XXX
out.settings_changePasswordNew = "New password"; // XXX
out.settings_changePasswordNewConfirm = "Confirm new password"; // XXX
out.settings_changePasswordConfirm = "Are you sure?"; // XXX
out.settings_ownDriveTitle = "Drive migration"; // XXX
out.settings_ownDriveHint = "Migrating your drive to the new version will give you access to new features..."; // XXX
out.settings_ownDriveButton = "Migrate"; // XXX
out.settings_ownDriveConfirm = "Are you sure?"; // XXX
out.settings_changePasswordTitle = "Change your password";
out.settings_changePasswordHint = "Change your account's password. Enter your current password, and confirm the new password by typing it twice.<br>" +
"<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_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_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_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.';
// 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." +
" 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 = {
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 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." +
" 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."
"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."
},
other: {
q: "What can other collaborators learn about me?",
@ -1174,6 +1184,7 @@ define(function () {
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_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_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.";

View file

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

View file

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

176
rpc.js
View file

@ -25,7 +25,7 @@ var SUPPRESS_RPC_ERRORS = false;
var WARN = function (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();
}
@ -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) {
if (!isValidId(channel)) { 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',
'REMOVE_OWNED_CHANNEL',
'REMOVE_PINS',
'WRITE_LOGIN_BLOCK',
'REMOVE_LOGIN_BLOCK',
].indexOf(call) !== -1;
};
@ -1423,6 +1578,7 @@ RPC.create = function (
var pinPath = paths.pin = keyOrDefaultString('pinPath', './pins');
var blobPath = paths.blob = keyOrDefaultString('blobPath', './blob');
var blobStagingPath = paths.staging = keyOrDefaultString('blobStagingPath', './blobstage');
paths.block = keyOrDefaultString('blockPath', './block');
var isUnauthenticateMessage = function (msg) {
return msg && msg.length === 2 && isUnauthenticatedCall(msg[0]);
@ -1559,7 +1715,7 @@ RPC.create = function (
var session = Sessions[safeKey];
var token = session? session.tokens.slice(-1)[0]: '';
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) {
@ -1692,6 +1848,22 @@ RPC.create = function (
WARN(e, 'UPLOAD_CANCEL');
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:
return void Respond('UNSUPPORTED_RPC_CALL', msg);
}

View file

@ -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')), {
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.dist'));
@ -162,7 +165,7 @@ app.get('/api/config', function(req, res){
res.send('define(function(){\n' + [
'var obj = ' + JSON.stringify({
requireConf: {
waitSeconds: 60,
waitSeconds: 600,
urlArgs: 'ver=' + Package.version + (FRESH_KEY? '-' + FRESH_KEY: '') + (DEV_MODE? '-' + (+new Date()): ''),
},
removeDonateButton: (config.removeDonateButton === true),

View file

@ -10,9 +10,13 @@ define([
'/common/wire.js',
'/common/flat-dom.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.Sortify = Sortify;
var Nacl = window.nacl;
var assertions = 0;
var failed = false;
@ -296,6 +300,15 @@ define([
!secret.hashData.present);
}, "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) {
try {
MediaTag(void 0).on('progress').on('decryption');

View file

@ -134,10 +134,15 @@ define([
};
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) {
if (css) {
inject(css, url);
return void cb();
return void done();
}
console.log('CACHE MISS ' + url);
((/\.less([\?\#].*)?$/.test(url)) ? loadLess : loadCSS)(url, function (err, css) {
@ -145,7 +150,7 @@ define([
var output = fixAllURLs(css, url);
cachePut(url, output);
inject(output, url);
cb();
done();
});
});
};

View file

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

View file

@ -147,7 +147,7 @@ define([
id: 'cp-app-prop-expire',
}));
var hasPassword = typeof data.password !== "undefined";
var hasPassword = data.password;
if (hasPassword) {
$('<label>', {'for': 'cp-app-prop-password'}).text(Messages.creation_passwordValue)
.appendTo($d);
@ -183,23 +183,31 @@ define([
passwordOk
]);
$(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) {
if (!yes) { return; }
sframeChan.query("Q_PAD_PASSWORD_CHANGE", {
href: data.href,
password: $(newPassword).find('input').val()
password: newPass
}, function (err, data) {
if (err || data.error) {
return void UI.alert(Messages.properties_passwordError);
}
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) {
return void UI.alert(Messages.properties_passwordWarning, function () {
common.gotoURL(hasPassword ? undefined : data.href);
common.gotoURL(hasPassword && newPass ? undefined : data.href);
}, {force: true});
}
return void UI.alert(Messages.properties_passwordSuccess, function () {
common.gotoURL(hasPassword ? undefined : data.href);
common.gotoURL(hasPassword && newPass ? undefined : data.href);
}, {force: true});
});
});

View file

@ -83,6 +83,21 @@ define([], function () {
}).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) {
var a = array.slice();
for(var i=0; i<a.length; i++) {
@ -122,17 +137,14 @@ define([], function () {
else if (bytes >= oneMegabyte) { return 'MB'; }
};
// given a path, asynchronously return an arraybuffer
Util.fetch = function (src, cb) {
var done = false;
var CB = function (err, res) {
if (done) { return; }
done = true;
cb(err, res);
};
var CB = Util.once(cb);
var xhr = new XMLHttpRequest();
xhr.open("GET", src, true);
xhr.responseType = "arraybuffer";
xhr.onerror = function (err) { CB(err); };
xhr.onload = function () {
if (/^4/.test(''+this.status)) {
return CB('XHR_ERROR');

View file

@ -8,11 +8,12 @@ define([
'/common/common-feedback.js',
'/common/outer/local-store.js',
'/common/outer/worker-channel.js',
'/common/outer/login-block.js',
'/customize/application_config.js',
'/bower_components/nthen/index.js',
], function (Config, Messages, Util, Hash,
Messaging, Constants, Feedback, LocalStore, Channel,
Messaging, Constants, Feedback, LocalStore, Channel, Block,
AppConfig, Nthen) {
/* 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
// SFRAME: talk to anon_rpc from the iframe
@ -615,10 +622,17 @@ define([
var warning = false;
var newHash;
var oldChannel;
if (parsed.hashData.password) {
newHash = parsed.hash;
var newSecret;
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 {
newHash = Hash.createRandomHash(parsed.type, newPassword);
newSecret = Hash.getSecrets(parsed.type, newHash, newPassword);
}
var newHref = '/' + parsed.type + '/#' + newHash;
@ -670,16 +684,17 @@ define([
return void cb(obj);
}
}));
common.unpinPads([oldChannel], waitFor());
common.pinPads([newSecret.channel], waitFor());
}).nThen(function (waitFor) {
common.setPadAttribute('password', newPassword, waitFor(function (err) {
if (err) { warning = true; }
}), href);
var secret = Hash.getSecrets(parsed.type, newHash, newPassword);
common.setPadAttribute('channel', secret.channel, waitFor(function (err) {
common.setPadAttribute('channel', newSecret.channel, waitFor(function (err) {
if (err) { warning = true; }
}), href);
if (parsed.hashData.password) { return; } // same hash
if (parsed.hashData.password && newPassword) { return; } // same hash
common.setPadAttribute('href', newHref, waitFor(function (err) {
if (err) { warning = true; }
}), 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
common.loading = {};
common.loading.onDriveEvent = Util.mkEvent();
@ -834,6 +1023,20 @@ define([
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 () {
var env = {};
var initialized = false;
@ -870,9 +1073,12 @@ define([
if (typeof(Worker) === "undefined") {
Feedback.send('NO_WEBWORKER');
}
if (typeof(ServiceWorker) === "undefined") {
if (!('serviceWorker' in navigator)) {
Feedback.send('NO_SERVICEWORKER');
}
if (!common.hasCSSVariables()) {
Feedback.send('NO_CSS_VARIABLES');
}
Feedback.reportScreenDimensions();
Feedback.reportLanguage();
@ -883,16 +1089,53 @@ define([
provideFeedback();
};
var userHash;
Nthen(function (waitFor) {
if (AppConfig.beforeLogin) {
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) {
var cfg = {
init: true,
userHash: LocalStore.getUserHash(),
userHash: userHash || LocalStore.getUserHash(),
anonHash: LocalStore.getFSHash(),
localToken: tryParsing(localStorage.getItem(Constants.tokenKey)),
localToken: tryParsing(localStorage.getItem(Constants.tokenKey)), // TODO move this to LocalStore ?
language: common.getLanguage(),
messenger: rdyCfg.messenger, // Boolean
driveEvents: rdyCfg.driveEvents // Boolean
@ -915,18 +1158,23 @@ define([
Nthen(function (waitFor2) {
if (Worker) {
var w = waitFor2();
worker = new Worker('/common/outer/testworker.js?' + urlArgs);
worker.onerror = function (errEv) {
errEv.preventDefault();
errEv.stopPropagation();
try {
worker = new Worker('/common/outer/testworker.js?' + urlArgs);
worker.onerror = function (errEv) {
errEv.preventDefault();
errEv.stopPropagation();
noWorker = true;
w();
};
worker.onmessage = function (ev) {
if (ev.data === "OK") {
w();
}
};
} catch (e) {
noWorker = true;
w();
};
worker.onmessage = function (ev) {
if (ev.data === "OK") {
w();
}
};
}
}
if (typeof(SharedWorker) !== "undefined") {
try {

View file

@ -13,7 +13,7 @@ define([
'/common/outer/network-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-listmap/chainpad-listmap.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) {
if (store.rpc) { return void cb(account); }
require(['/common/pinpad.js'], function (Pinpad) {

View file

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

View file

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

View file

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

View file

@ -563,34 +563,41 @@ define([
continue;
}
// Clean missing href
if (!el.href) {
var parsed;
if (el.href) {
if (!el.href) {
debug("Removing an element in filesData with a missing href.", el);
toClean.push(id);
continue;
}
parsed = Hash.parsePadUrl(el.href);
// Clean invalid hash
if (!parsed.hash) {
debug("Removing an element in filesData with a invalid href.", el);
toClean.push(id);
continue;
}
// Clean invalid type
if (!parsed.type) {
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);
continue;
}
var parsed = Hash.parsePadUrl(el.href);
// Clean invalid hash
if (!parsed.hash) {
debug("Removing an element in filesData with a invalid href.", el);
toClean.push(id);
continue;
}
// Clean invalid type
if (!parsed.type) {
debug("Removing an element in filesData with a invalid type.", el);
toClean.push(id);
continue;
}
// 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
if (!el.ctime) { el.ctime = el.atime; }
// Fix title
if (!el.title) { el.title = Hash.getDefaultName(parsed); }
// Fix channel
if (!el.channel) {
if (el.href && !el.channel) {
try {
var secret = Hash.getSecrets(parsed.type, parsed.hash, el.password);
el.channel = secret.channel;

View file

@ -222,7 +222,34 @@ define([
};
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);

View file

@ -126,7 +126,7 @@ define([
if (newState === STATE.INFINITE_SPINNER || newState === STATE.DELETED) {
state = newState;
} 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) {
throw new Error("Cannot transition from " + state + " to " + newState);
} else {

View file

@ -661,10 +661,18 @@ define([
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) {
Cryptpad.writeLoginBlock(data, cb);
});
sframeChan.on('Q_REMOVE_LOGIN_BLOCK', function (data, cb) {
Cryptpad.removeLoginBlock(data, cb);
});
if (cfg.addRpc) {
cfg.addRpc(sframeChan, Cryptpad, Utils);
}

View file

@ -77,6 +77,9 @@ define({
// Write/update the login block when the account password is changed
'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.
// display a warning
'Q_GET_PIN_LIMIT_STATUS': true,
@ -235,6 +238,9 @@ define({
// Change pad password
'Q_PAD_PASSWORD_CHANGE': true,
// Migrate drive to owned drive
'Q_CHANGE_USER_PASSWORD': true,
// Loading events to display in the loading screen
'EV_LOADING_INFO': true,
// Critical error outside the iframe during loading screen

View file

@ -37,7 +37,14 @@ define([
var logError = config.logError || logging;
var debug = exp.debug = config.debug || logging;
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);
};

View file

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

View file

@ -326,7 +326,21 @@ define([
if (framework.isReadOnly()) {
$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) {
if (framework.isReadOnly()) { return; }
if (!kanban) { return; }

View file

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

View file

@ -50,7 +50,7 @@ define([
'cp-settings-resettips',
'cp-settings-thumbnails',
'cp-settings-userfeedback',
//'cp-settings-change-password',
'cp-settings-change-password',
'cp-settings-delete'
],
'creation': [
@ -404,12 +404,11 @@ define([
$(form).appendTo($div);
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}); }
cb (obj);
});
};
updateBlock = updateBlock; // jshint..
var todo = function () {
var oldPassword = $(form).find('#cp-settings-change-password-current').val();
@ -432,8 +431,21 @@ define([
UI.confirm(Messages.settings_changePasswordConfirm,
function (yes) {
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,
cancel: Messages.register_cancel,
@ -461,6 +473,50 @@ define([
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
var setHTML = function (e, html) {

View file

@ -333,15 +333,6 @@ define([
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
var onUpload = function (e) {
var file = e.target.files[0];
@ -390,6 +381,15 @@ define([
};
framework._.sfCommon.openFilePicker(pickerCfg);
}).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()) {