login.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. define([
  2. 'jquery',
  3. '/bower_components/chainpad-listmap/chainpad-listmap.js',
  4. '/bower_components/chainpad-crypto/crypto.js',
  5. '/common/common-util.js',
  6. '/common/outer/network-config.js',
  7. '/customize/credential.js',
  8. '/bower_components/chainpad/chainpad.dist.js',
  9. '/common/common-realtime.js',
  10. '/common/common-constants.js',
  11. '/common/common-interface.js',
  12. '/common/common-feedback.js',
  13. '/common/outer/local-store.js',
  14. '/customize/messages.js',
  15. '/bower_components/nthen/index.js',
  16. '/common/outer/login-block.js',
  17. '/common/common-hash.js',
  18. '/bower_components/tweetnacl/nacl-fast.min.js',
  19. '/bower_components/scrypt-async/scrypt-async.min.js', // better load speed
  20. ], function ($, Listmap, Crypto, Util, NetConfig, Cred, ChainPad, Realtime, Constants, UI,
  21. Feedback, LocalStore, Messages, nThen, Block, Hash) {
  22. var Exports = {
  23. Cred: Cred,
  24. // this is depended on by non-customizable files
  25. // be careful when modifying login.js
  26. requiredBytes: 192,
  27. };
  28. var Nacl = window.nacl;
  29. var allocateBytes = Exports.allocateBytes = function (bytes) {
  30. var dispense = Cred.dispenser(bytes);
  31. var opt = {};
  32. // dispense 18 bytes of entropy for your encryption key
  33. var encryptionSeed = dispense(18);
  34. // 16 bytes for a deterministic channel key
  35. var channelSeed = dispense(16);
  36. // 32 bytes for a curve key
  37. var curveSeed = dispense(32);
  38. var curvePair = Nacl.box.keyPair.fromSecretKey(new Uint8Array(curveSeed));
  39. opt.curvePrivate = Nacl.util.encodeBase64(curvePair.secretKey);
  40. opt.curvePublic = Nacl.util.encodeBase64(curvePair.publicKey);
  41. // 32 more for a signing key
  42. var edSeed = opt.edSeed = dispense(32);
  43. // 64 more bytes to seed an additional signing key
  44. var blockKeys = opt.blockKeys = Block.genkeys(new Uint8Array(dispense(64)));
  45. opt.blockHash = Block.getBlockHash(blockKeys);
  46. // derive a private key from the ed seed
  47. var signingKeypair = Nacl.sign.keyPair.fromSeed(new Uint8Array(edSeed));
  48. opt.edPrivate = Nacl.util.encodeBase64(signingKeypair.secretKey);
  49. opt.edPublic = Nacl.util.encodeBase64(signingKeypair.publicKey);
  50. var keys = opt.keys = Crypto.createEditCryptor(null, encryptionSeed);
  51. // 24 bytes of base64
  52. keys.editKeyStr = keys.editKeyStr.replace(/\//g, '-');
  53. // 32 bytes of hex
  54. var channelHex = opt.channelHex = Util.uint8ArrayToHex(channelSeed);
  55. // should never happen
  56. if (channelHex.length !== 32) { throw new Error('invalid channel id'); }
  57. var channel64 = Util.hexToBase64(channelHex);
  58. // we still generate a v1 hash because this function needs to deterministically
  59. // derive the same values as it always has. New accounts will generate their own
  60. // userHash values
  61. opt.userHash = '/1/edit/' + [channel64, opt.keys.editKeyStr].join('/') + '/';
  62. return opt;
  63. };
  64. var loginOptionsFromBlock = function (blockInfo) {
  65. var opt = {};
  66. var parsed = Hash.getSecrets('pad', blockInfo.User_hash);
  67. opt.channelHex = parsed.channel;
  68. opt.keys = parsed.keys;
  69. opt.edPublic = blockInfo.edPublic;
  70. opt.User_name = blockInfo.User_name;
  71. return opt;
  72. };
  73. var loadUserObject = function (opt, cb) {
  74. var config = {
  75. websocketURL: NetConfig.getWebsocketURL(),
  76. channel: opt.channelHex,
  77. data: {},
  78. validateKey: opt.keys.validateKey, // derived validation key
  79. crypto: Crypto.createEncryptor(opt.keys),
  80. logLevel: 1,
  81. classic: true,
  82. ChainPad: ChainPad,
  83. owners: [opt.edPublic]
  84. };
  85. var rt = opt.rt = Listmap.create(config);
  86. rt.proxy
  87. .on('ready', function () {
  88. setTimeout(function () { cb(void 0, rt); });
  89. })
  90. .on('disconnect', function (info) {
  91. cb('E_DISCONNECT', info);
  92. });
  93. };
  94. var isProxyEmpty = function (proxy) {
  95. var l = Object.keys(proxy).length;
  96. return l === 0 || (l === 2 && proxy._events && proxy.on);
  97. };
  98. var setMergeAnonDrive = function () {
  99. sessionStorage.migrateAnonDrive = 1;
  100. };
  101. var setCreateReadme = function () {
  102. sessionStorage.createReadme = 1;
  103. };
  104. Exports.loginOrRegister = function (uname, passwd, isRegister, shouldImport, cb) {
  105. if (typeof(cb) !== 'function') { return; }
  106. // Usernames are all lowercase. No going back on this one
  107. uname = uname.toLowerCase();
  108. // validate inputs
  109. if (!Cred.isValidUsername(uname)) { return void cb('INVAL_USER'); }
  110. if (!Cred.isValidPassword(passwd)) { return void cb('INVAL_PASS'); }
  111. if (isRegister && !Cred.isLongEnoughPassword(passwd)) {
  112. return void cb('PASS_TOO_SHORT');
  113. }
  114. // results...
  115. var res = {
  116. register: isRegister,
  117. };
  118. var RT, blockKeys, blockHash, Pinpad, rpc, userHash;
  119. nThen(function (waitFor) {
  120. // derive a predefined number of bytes from the user's inputs,
  121. // and allocate them in a deterministic fashion
  122. Cred.deriveFromPassphrase(uname, passwd, Exports.requiredBytes, waitFor(function (bytes) {
  123. res.opt = allocateBytes(bytes);
  124. blockHash = res.opt.blockHash;
  125. blockKeys = res.opt.blockKeys;
  126. }));
  127. }).nThen(function (waitFor) {
  128. // the allocated bytes can be used either in a legacy fashion,
  129. // or in such a way that a previously unused byte range determines
  130. // the location of a layer of indirection which points users to
  131. // an encrypted block, from which they can recover the location of
  132. // the rest of their data
  133. // determine where a block for your set of keys would be stored
  134. var blockUrl = Block.getBlockUrl(res.opt.blockKeys);
  135. // Check whether there is a block at that location
  136. Util.fetch(blockUrl, waitFor(function (err, block) {
  137. // if users try to log in or register, we must check
  138. // whether there is a block.
  139. // the block is only useful if it can be decrypted, though
  140. if (err) {
  141. console.log("no block found");
  142. return;
  143. }
  144. var decryptedBlock = Block.decrypt(block, blockKeys);
  145. if (!decryptedBlock) {
  146. console.error("Found a login block but failed to decrypt");
  147. return;
  148. }
  149. console.error(decryptedBlock);
  150. res.blockInfo = decryptedBlock;
  151. }));
  152. }).nThen(function (waitFor) {
  153. // we assume that if there is a block, it was created in a valid manner
  154. // so, just proceed to the next block which handles that stuff
  155. if (res.blockInfo) { return; }
  156. var opt = res.opt;
  157. // load the user's object using the legacy credentials
  158. loadUserObject(opt, waitFor(function (err, rt) {
  159. if (err) {
  160. waitFor.abort();
  161. return void cb(err);
  162. }
  163. // if a proxy is marked as deprecated, it is because someone had a non-owned drive
  164. // but changed their password, and couldn't delete their old data.
  165. // if they are here, they have entered their old credentials, so we should not
  166. // allow them to proceed. In time, their old drive should get deleted, since
  167. // it will should be pinned by anyone's drive.
  168. if (rt.proxy[Constants.deprecatedKey]) {
  169. waitFor.abort();
  170. return void cb('NO_SUCH_USER', res);
  171. }
  172. if (isRegister && isProxyEmpty(rt.proxy)) {
  173. // If they are trying to register,
  174. // and the proxy is empty, then there is no 'legacy user' either
  175. // so we should just shut down this session and disconnect.
  176. rt.network.disconnect();
  177. return; // proceed to the next async block
  178. }
  179. // they tried to just log in but there's no such user
  180. // and since we're here at all there is no modern-block
  181. if (!isRegister && isProxyEmpty(rt.proxy)) {
  182. rt.network.disconnect(); // clean up after yourself
  183. waitFor.abort();
  184. return void cb('NO_SUCH_USER', res);
  185. }
  186. // they tried to register, but those exact credentials exist
  187. if (isRegister && !isProxyEmpty(rt.proxy)) {
  188. rt.network.disconnect();
  189. waitFor.abort();
  190. Feedback.send('LOGIN', true);
  191. return void cb('ALREADY_REGISTERED', res);
  192. }
  193. // if you are here, then there is no block, the user is trying
  194. // to log in. The proxy is **not** empty. All values assigned here
  195. // should have been deterministically created using their credentials
  196. // so setting them is just a precaution to keep things in good shape
  197. res.proxy = rt.proxy;
  198. res.realtime = rt.realtime;
  199. // they're registering...
  200. res.userHash = opt.userHash;
  201. res.userName = uname;
  202. // export their signing key
  203. res.edPrivate = opt.edPrivate;
  204. res.edPublic = opt.edPublic;
  205. // export their encryption key
  206. res.curvePrivate = opt.curvePrivate;
  207. res.curvePublic = opt.curvePublic;
  208. if (shouldImport) { setMergeAnonDrive(); }
  209. // don't proceed past this async block.
  210. waitFor.abort();
  211. // We have to call whenRealtimeSyncs asynchronously here because in the current
  212. // version of listmap, onLocal calls `chainpad.contentUpdate(newValue)`
  213. // asynchronously.
  214. // The following setTimeout is here to make sure whenRealtimeSyncs is called after
  215. // `contentUpdate` so that we have an update userDoc in chainpad.
  216. setTimeout(function () {
  217. Realtime.whenRealtimeSyncs(rt.realtime, function () {
  218. // the following stages are there to initialize a new drive
  219. // if you are registering
  220. LocalStore.login(res.userHash, res.userName, function () {
  221. setTimeout(function () { cb(void 0, res); });
  222. });
  223. });
  224. });
  225. }));
  226. }).nThen(function (waitFor) { // MODERN REGISTRATION / LOGIN
  227. var opt;
  228. if (res.blockInfo) {
  229. opt = loginOptionsFromBlock(res.blockInfo);
  230. userHash = res.blockInfo.User_hash;
  231. console.error(opt, userHash);
  232. } else {
  233. console.log("allocating random bytes for a new user object");
  234. opt = allocateBytes(Nacl.randomBytes(Exports.requiredBytes));
  235. // create a random v2 hash, since we don't need backwards compatibility
  236. userHash = opt.userHash = Hash.createRandomHash('drive');
  237. var secret = Hash.getSecrets('drive', userHash);
  238. opt.keys = secret.keys;
  239. opt.channelHex = secret.channel;
  240. }
  241. // according to the location derived from the credentials which you entered
  242. loadUserObject(opt, waitFor(function (err, rt) {
  243. if (err) {
  244. waitFor.abort();
  245. return void cb('MODERN_REGISTRATION_INIT');
  246. }
  247. console.error(JSON.stringify(rt.proxy));
  248. // export the realtime object you checked
  249. RT = rt;
  250. var proxy = rt.proxy;
  251. if (isRegister && !isProxyEmpty(proxy) && (!proxy.edPublic || !proxy.edPrivate)) {
  252. console.error("INVALID KEYS");
  253. console.log(JSON.stringify(proxy));
  254. return;
  255. }
  256. res.proxy = rt.proxy;
  257. res.realtime = rt.realtime;
  258. // they're registering...
  259. res.userHash = userHash;
  260. res.userName = uname;
  261. // somehow they have a block present, but nothing in the user object it specifies
  262. // this shouldn't happen, but let's send feedback if it does
  263. if (!isRegister && isProxyEmpty(rt.proxy)) {
  264. // this really shouldn't happen, but let's handle it anyway
  265. Feedback.send('EMPTY_LOGIN_WITH_BLOCK');
  266. rt.network.disconnect(); // clean up after yourself
  267. waitFor.abort();
  268. return void cb('NO_SUCH_USER', res);
  269. }
  270. // they tried to register, but those exact credentials exist
  271. if (isRegister && !isProxyEmpty(rt.proxy)) {
  272. rt.network.disconnect();
  273. waitFor.abort();
  274. res.blockHash = blockHash;
  275. if (shouldImport) {
  276. setMergeAnonDrive();
  277. }
  278. return void cb('ALREADY_REGISTERED', res);
  279. }
  280. if (!isRegister && !isProxyEmpty(rt.proxy)) {
  281. LocalStore.setBlockHash(blockHash);
  282. waitFor.abort();
  283. if (shouldImport) {
  284. setMergeAnonDrive();
  285. }
  286. return void LocalStore.login(userHash, uname, function () {
  287. cb(void 0, res);
  288. });
  289. }
  290. if (isRegister && isProxyEmpty(rt.proxy)) {
  291. proxy.edPublic = opt.edPublic;
  292. proxy.edPrivate = opt.edPrivate;
  293. proxy.curvePublic = opt.curvePublic;
  294. proxy.curvePrivate = opt.curvePrivate;
  295. proxy.login_name = uname;
  296. proxy[Constants.displayNameKey] = uname;
  297. setCreateReadme();
  298. if (shouldImport) {
  299. setMergeAnonDrive();
  300. } else {
  301. proxy.version = 6;
  302. }
  303. Feedback.send('REGISTRATION', true);
  304. } else {
  305. Feedback.send('LOGIN', true);
  306. }
  307. setTimeout(waitFor(function () {
  308. Realtime.whenRealtimeSyncs(rt.realtime, waitFor());
  309. }));
  310. }));
  311. }).nThen(function (waitFor) {
  312. require(['/common/pinpad.js'], waitFor(function (_Pinpad) {
  313. console.log("loaded rpc module");
  314. Pinpad = _Pinpad;
  315. }));
  316. }).nThen(function (waitFor) {
  317. // send an RPC to store the block which you created.
  318. console.log("initializing rpc interface");
  319. Pinpad.create(RT.network, RT.proxy, waitFor(function (e, _rpc) {
  320. if (e) {
  321. waitFor.abort();
  322. console.error(e); // INVALID_KEYS
  323. return void cb('RPC_CREATION_ERROR');
  324. }
  325. rpc = _rpc;
  326. console.log("rpc initialized");
  327. }));
  328. }).nThen(function (waitFor) {
  329. console.log("creating request to publish a login block");
  330. // Finally, create the login block for the object you just created.
  331. var toPublish = {};
  332. toPublish[Constants.userNameKey] = uname;
  333. toPublish[Constants.userHashKey] = userHash;
  334. toPublish.edPublic = RT.proxy.edPublic;
  335. var blockRequest = Block.serialize(JSON.stringify(toPublish), res.opt.blockKeys);
  336. rpc.writeLoginBlock(blockRequest, waitFor(function (e) {
  337. if (e) { return void console.error(e); }
  338. console.log("blockInfo available at:", blockHash);
  339. LocalStore.setBlockHash(blockHash);
  340. LocalStore.login(userHash, uname, function () {
  341. cb(void 0, res);
  342. });
  343. }));
  344. });
  345. };
  346. Exports.redirect = function () {
  347. if (sessionStorage.redirectTo) {
  348. var h = sessionStorage.redirectTo;
  349. var parser = document.createElement('a');
  350. parser.href = h;
  351. if (parser.origin === window.location.origin) {
  352. delete sessionStorage.redirectTo;
  353. window.location.href = h;
  354. return;
  355. }
  356. }
  357. window.location.href = '/drive/';
  358. };
  359. var hashing;
  360. Exports.loginOrRegisterUI = function (uname, passwd, isRegister, shouldImport, testing, test) {
  361. if (hashing) { return void console.log("hashing is already in progress"); }
  362. hashing = true;
  363. var proceed = function (result) {
  364. hashing = false;
  365. if (test && typeof test === "function" && test()) { return; }
  366. Realtime.whenRealtimeSyncs(result.realtime, function () {
  367. Exports.redirect();
  368. });
  369. };
  370. // setTimeout 100ms to remove the keyboard on mobile devices before the loading screen
  371. // pops up
  372. window.setTimeout(function () {
  373. UI.addLoadingScreen({
  374. loadingText: Messages.login_hashing,
  375. hideTips: true,
  376. });
  377. // We need a setTimeout(cb, 0) otherwise the loading screen is only displayed
  378. // after hashing the password
  379. window.setTimeout(function () {
  380. Exports.loginOrRegister(uname, passwd, isRegister, shouldImport, function (err, result) {
  381. var proxy;
  382. if (result) { proxy = result.proxy; }
  383. if (err) {
  384. switch (err) {
  385. case 'NO_SUCH_USER':
  386. UI.removeLoadingScreen(function () {
  387. UI.alert(Messages.login_noSuchUser, function () {
  388. hashing = false;
  389. });
  390. });
  391. break;
  392. case 'INVAL_USER':
  393. UI.removeLoadingScreen(function () {
  394. UI.alert(Messages.login_invalUser, function () {
  395. hashing = false;
  396. });
  397. });
  398. break;
  399. case 'INVAL_PASS':
  400. UI.removeLoadingScreen(function () {
  401. UI.alert(Messages.login_invalPass, function () {
  402. hashing = false;
  403. });
  404. });
  405. break;
  406. case 'PASS_TOO_SHORT':
  407. UI.removeLoadingScreen(function () {
  408. var warning = Messages._getKey('register_passwordTooShort', [
  409. Cred.MINIMUM_PASSWORD_LENGTH
  410. ]);
  411. UI.alert(warning, function () {
  412. hashing = false;
  413. });
  414. });
  415. break;
  416. case 'ALREADY_REGISTERED':
  417. UI.removeLoadingScreen(function () {
  418. UI.confirm(Messages.register_alreadyRegistered, function (yes) {
  419. if (!yes) {
  420. hashing = false;
  421. return;
  422. }
  423. proxy.login_name = uname;
  424. if (!proxy[Constants.displayNameKey]) {
  425. proxy[Constants.displayNameKey] = uname;
  426. }
  427. LocalStore.eraseTempSessionValues();
  428. if (result.blockHash) {
  429. LocalStore.setBlockHash(result.blockHash);
  430. }
  431. LocalStore.login(result.userHash, result.userName, function () {
  432. setTimeout(function () { proceed(result); });
  433. });
  434. });
  435. });
  436. break;
  437. default: // UNHANDLED ERROR
  438. hashing = false;
  439. UI.errorLoadingScreen(Messages.login_unhandledError);
  440. }
  441. return;
  442. }
  443. if (testing) { return void proceed(result); }
  444. if (!(proxy.curvePrivate && proxy.curvePublic &&
  445. proxy.edPrivate && proxy.edPublic)) {
  446. console.log("recovering derived public/private keypairs");
  447. // **** reset keys ****
  448. proxy.curvePrivate = result.curvePrivate;
  449. proxy.curvePublic = result.curvePublic;
  450. proxy.edPrivate = result.edPrivate;
  451. proxy.edPublic = result.edPublic;
  452. }
  453. setTimeout(function () {
  454. Realtime.whenRealtimeSyncs(result.realtime, function () {
  455. proceed(result);
  456. });
  457. });
  458. });
  459. }, 500);
  460. }, 200);
  461. };
  462. return Exports;
  463. });