PageRenderTime 47ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/src/mobile/sgp.mobile.js

https://github.com/airportyh/supergenpass
JavaScript | 437 lines | 267 code | 92 blank | 78 comment | 39 complexity | e3e4cd9d7b55be6d2373db64885d8860 MD5 | raw file
Possible License(s): AGPL-1.0
  1. 'use strict';
  2. // Load requirements.
  3. var $ = require('jquery');
  4. var sgp = require('supergenpass-lib');
  5. var md5 = require('crypto-js/md5');
  6. var sha512 = require('crypto-js/sha512');
  7. var zeroclipboard = require('zeroclipboard');
  8. var flashversion = require('./lib/flashversion');
  9. var identicon = require('./lib/identicon5');
  10. var shortcut = require('./lib/shortcut');
  11. var storage = require('./lib/localstorage-polyfill');
  12. // Set default values.
  13. var messageOrigin = false;
  14. var messageSource = false;
  15. var language = location.search.substring(1);
  16. var latestBookmarklet = '../bookmarklet/bookmarklet.min.js';
  17. var latestVersion = 20140728;
  18. var alternateDomain = '';
  19. // ZeroClipboard configuration.
  20. var zeroClipboardConfig = {
  21. bubbleEvents: false,
  22. hoverClass: 'Hover',
  23. activeClass: 'Active'
  24. };
  25. // Major search engine referral hostnames.
  26. var searchEngines = [
  27. 'www.google.com',
  28. 'www.bing.com',
  29. 'duckduckgo.com',
  30. 'r.search.yahoo.com'
  31. ];
  32. // Localizations.
  33. var localizations = {
  34. 'en': ['Master password', 'Domain / URL', 'Generate'],
  35. 'es': ['Contraseña maestra', 'Dominio / URL', 'Enviar'],
  36. 'fr': ['Mot de passe principal', 'Domaine / URL', 'Soumettre'],
  37. 'de': ['Master Passwort', 'Domain / URL', 'Abschicken'],
  38. 'pt-br': ['Senha-mestra', 'Domínio / URL', 'Gerar'],
  39. 'zh-hk': ['主密碼', '域名 / URL', '提交'],
  40. 'hu': ['Mesterjelszó', 'Tartomány / Internetcím', 'OK'],
  41. 'ru': ['Мастер-пароль', 'Домена / URL', 'Подтвердить']
  42. };
  43. // Enumerate jQuery selectors for caching.
  44. var $el = {};
  45. var selectors =
  46. [
  47. 'PasswdField',
  48. 'Passwd',
  49. 'PasswdLabel',
  50. 'Secret',
  51. 'DomainField',
  52. 'Domain',
  53. 'DomainLabel',
  54. 'DisableTLD',
  55. 'Len',
  56. 'Generate',
  57. 'Mask',
  58. 'MaskText',
  59. 'CopyButton',
  60. 'Output',
  61. 'Canvas',
  62. 'Options',
  63. 'Update',
  64. 'Bookmarklet'
  65. ];
  66. // Retrieve user's configuration from local storage, if available.
  67. var config = {
  68. passwordLength: storage.local.getItem('Len') || 10,
  69. masterSecret: storage.local.getItem('Salt') || '',
  70. hashMethod: storage.local.getItem('Method') || 'md5',
  71. disableTLD: storage.local.getItem('DisableTLD') || ''
  72. };
  73. var showUpdateNotification = function (data) {
  74. $el.Bookmarklet.attr('href', data);
  75. $el.Update.show();
  76. sendDocumentHeight();
  77. };
  78. // Populate domain with referrer, if available and not from a search engine.
  79. var populateReferrer = function (referrer) {
  80. if (referrer) {
  81. referrer = sgp.hostname(referrer, {removeSubdomains: false});
  82. if (searchEngines.indexOf(referrer) === -1) {
  83. $el.Domain.val(sgp.hostname(referrer, {removeSubdomains: !config.disableTLD}));
  84. }
  85. }
  86. };
  87. // Listen for postMessage from bookmarklet.
  88. var listenForBookmarklet = function (event) {
  89. // Gather information.
  90. var post = event.originalEvent;
  91. messageSource = post.source;
  92. messageOrigin = post.origin;
  93. // Parse message.
  94. $.each(JSON.parse(post.data), function (key, value) {
  95. switch (key) {
  96. case 'version':
  97. if(value < latestVersion) {
  98. // Fetch latest bookmarklet.
  99. $.ajax({
  100. url: latestBookmarklet,
  101. success: showUpdateNotification,
  102. dataType: 'html'
  103. });
  104. }
  105. break;
  106. }
  107. });
  108. // Populate domain field and call back with the browser height.
  109. $el.Domain.val(sgp.hostname(messageOrigin, {removeSubdomains: !config.disableTLD})).trigger('change');
  110. sendDocumentHeight();
  111. };
  112. // Send document height to bookmarklet.
  113. var sendDocumentHeight = function () {
  114. postMessageToBookmarklet({
  115. height: $(document.body).height()
  116. });
  117. };
  118. // Send generated password to bookmarklet.
  119. var sendGeneratedPassword = function (generatedPassword) {
  120. postMessageToBookmarklet({
  121. result: generatedPassword
  122. });
  123. };
  124. // Send message using HTML5 postMessage API. Only post a message if we are in
  125. // communication with the bookmarklet.
  126. var postMessageToBookmarklet = function (message) {
  127. if(messageSource && messageOrigin) {
  128. messageSource.postMessage(JSON.stringify(message), messageOrigin);
  129. }
  130. };
  131. // Save configuration to local storage.
  132. var saveConfiguration = function (masterSecret, passwordLength, hashMethod, disableTLD) {
  133. storage.local.setItem('Salt', masterSecret);
  134. storage.local.setItem('Len', passwordLength);
  135. storage.local.setItem('Method', hashMethod);
  136. storage.local.setItem('DisableTLD', disableTLD || '');
  137. };
  138. // Get selected hash method.
  139. var getHashMethod = function () {
  140. return $('input:radio[name=Method]:checked').val() || 'md5';
  141. };
  142. // Validate password length.
  143. var validatePasswordLength = function (passwordLength) {
  144. // Password length must be an integer.
  145. passwordLength = parseInt(passwordLength, 10) || 10;
  146. // Return a password length in the valid range.
  147. return Math.max(4, Math.min(passwordLength, 24));
  148. };
  149. // Generate hexadecimal hash for identicons.
  150. var generateIdenticonHash = function (seed, hashMethod) {
  151. // Store reference to the hash function.
  152. var hashFunction = ( hashMethod == 'sha512' ) ? sha512 : md5;
  153. // Loop four times over the seed.
  154. for (var i = 0; i <= 4; i++) {
  155. seed = hashFunction(seed).toString();
  156. }
  157. return seed;
  158. };
  159. // Generate and show identicon if master password or secret is present.
  160. var generateIdenticon = function () {
  161. // Get form input.
  162. var masterPassword = $el.Passwd.val();
  163. var masterSecret = $el.Secret.val();
  164. var hashMethod = getHashMethod();
  165. if(masterPassword || masterSecret) {
  166. // Compute identicon hash.
  167. var identiconHash = generateIdenticonHash(masterPassword + masterSecret, hashMethod);
  168. // Generate identicon.
  169. identicon($el.Canvas[0], identiconHash, 16);
  170. // Show identicon.
  171. $el.Canvas.show();
  172. } else {
  173. // Hide identicon if there is no form input.
  174. $el.Canvas.hide();
  175. }
  176. };
  177. var generatePassword = function () {
  178. // Get form input.
  179. var masterPassword = $el.Passwd.val();
  180. var masterSecret = $el.Secret.val();
  181. var hashMethod = getHashMethod();
  182. var domain = $el.Domain.val().replace(/ /g, '');
  183. var passwordLength = validatePasswordLength($el.Len.val());
  184. var disableTLD = $el.DisableTLD.is(':checked');
  185. // Process domain value.
  186. domain = (domain) ? sgp.hostname(domain, {removeSubdomains: !disableTLD}) : '';
  187. alternateDomain = (domain) ? sgp.hostname(domain, {removeSubdomains: disableTLD}) : '';
  188. // Update form with validated input.
  189. $el.Domain.val(domain).trigger('change');
  190. $el.Len.val(passwordLength).trigger('change');
  191. // Show user feedback for missing master password.
  192. if(!masterPassword) {
  193. $el.PasswdField.addClass('Missing');
  194. }
  195. // Show user feedback for missing domain.
  196. if(!domain) {
  197. $el.DomainField.addClass('Missing');
  198. }
  199. // Generate password.
  200. if(masterPassword && domain) {
  201. // Compile SGP options hash.
  202. var options = {
  203. secret: masterSecret,
  204. length: passwordLength,
  205. method: hashMethod,
  206. removeSubdomains: !disableTLD
  207. };
  208. // Generate password.
  209. var generatedPassword = sgp(masterPassword, domain, options);
  210. // Send generated password to bookmarklet.
  211. sendGeneratedPassword(generatedPassword);
  212. // Save form input to local storage.
  213. saveConfiguration(masterSecret, passwordLength, hashMethod, disableTLD);
  214. // Blur input fields.
  215. $el.Inputs.trigger('blur');
  216. // Show masked generated password.
  217. $el.Generate.hide();
  218. $el.Output.text(generatedPassword);
  219. $el.Mask.show();
  220. // Bind hotkey for revealing generated password.
  221. shortcut.add('Ctrl+H', toggleGeneratedPassword);
  222. }
  223. };
  224. // Toggle generated password on click/touch.
  225. var toggleGeneratedPassword = function () {
  226. $el.Mask.toggle();
  227. $el.Output.toggle();
  228. };
  229. // Clear generated password when input changes.
  230. var clearGeneratedPassword = function (event) {
  231. // Store reference to key press.
  232. var key = event.which;
  233. // Test for input key codes.
  234. var group1 = ([8, 32].indexOf(key) !== -1);
  235. var group2 = (key > 45 && key < 91);
  236. var group3 = (key > 95 && key < 112);
  237. var group4 = (key > 185 && key < 223);
  238. var enterKey = (key == 13);
  239. // When user enters form input, reset form status.
  240. if ( event.type == 'change' || group1 || group2 || group3 || group4 ) {
  241. // Clear generated password.
  242. $el.Mask.hide();
  243. $el.Output.text('').hide();
  244. // Show generate button.
  245. $el.Generate.show();
  246. // Clear feedback for missing form input.
  247. $el.PasswdField.removeClass('Missing');
  248. $el.DomainField.removeClass('Missing');
  249. // Unbind hotkey for revealing generated password.
  250. shortcut.remove('Ctrl+H');
  251. }
  252. // Submit form on enter key.
  253. if (enterKey) {
  254. $el.Generate.trigger('click');
  255. event.preventDefault();
  256. }
  257. };
  258. // Adjust password length.
  259. var adjustPasswordLength = function (event) {
  260. // Get length increment.
  261. var increment = ( $(this).attr('id') == 'Up' ) ? 1 : -1;
  262. // Calculate new password length.
  263. var passwordLength = validatePasswordLength($el.Len.val());
  264. var newPasswordLength = validatePasswordLength(passwordLength + increment);
  265. // Update form with new password length.
  266. $el.Len.val(newPasswordLength).trigger('change');
  267. // Prevent event default action.
  268. event.preventDefault();
  269. };
  270. // Toggle advanced options.
  271. var toggleAdvancedOptions = function () {
  272. $('body').toggleClass('Advanced');
  273. sendDocumentHeight();
  274. };
  275. // Toggle alternate domain when TLD option is toggled.
  276. var toggleAlternateDomain = function () {
  277. // Store current domain value.
  278. var currentDomain = $el.Domain.val();
  279. // If we have stored an alternate value for the domain, load it.
  280. if (alternateDomain) {
  281. $el.Domain.val(alternateDomain);
  282. }
  283. // Store the current value as an alternate value in case the user toggles back.
  284. alternateDomain = currentDomain;
  285. };
  286. // Toggle indicator for TLD option.
  287. var toggleTLDIndicator = function () {
  288. $el.DomainField.toggleClass('Advanced', $(this).is(':checked'));
  289. };
  290. // Update copy button to show successful clipboard copy. Remove success
  291. // indicator after a few seconds.
  292. var updateCopyButton = function () {
  293. $el.CopyButton.addClass('Success');
  294. setTimeout(function () {
  295. $el.CopyButton.removeClass('Success');
  296. }, 5000);
  297. };
  298. // Populate selector cache.
  299. $el.Inputs = $('input');
  300. $.each(selectors, function (i, val) {
  301. $el[val] = $('#' + val);
  302. });
  303. // Load user's configuration (or defaults) into form.
  304. $('input:radio[value=' + config.hashMethod + ']').prop('checked', true);
  305. $el.Len.val(validatePasswordLength(config.passwordLength));
  306. $el.Secret.val(config.masterSecret).trigger('change');
  307. $el.DisableTLD.prop('checked', config.disableTLD).trigger('change');
  308. // Perform localization, if requested.
  309. if (language && localizations.hasOwnProperty(language)) {
  310. $el.Passwd.attr('placeholder', localizations[language][0]);
  311. $el.Domain.attr('placeholder', localizations[language][1]);
  312. $el.PasswdLabel.text(localizations[language][0]);
  313. $el.DomainLabel.text(localizations[language][1]);
  314. $el.Generate.text(localizations[language][2]);
  315. }
  316. // Provide fake input placeholders if browser does not support them.
  317. if ( !('placeholder' in document.createElement('input')) ) {
  318. $('#Passwd, #Secret, #Domain').on('keyup change', function () {
  319. $('label[for=' + $(this).attr('id') + ']').toggle($(this).val() === '');
  320. }).trigger('change');
  321. }
  322. // Activate copy-to-clipboard button if browser has Flash.
  323. if (flashversion >= 11) {
  324. zeroclipboard.config(zeroClipboardConfig);
  325. new zeroclipboard($el.CopyButton.show()).on('aftercopy', updateCopyButton);
  326. }
  327. // Bind to interaction events.
  328. $el.Generate.on('click', generatePassword);
  329. $el.MaskText.on('click', toggleGeneratedPassword);
  330. $el.Options.on('click', toggleAdvancedOptions);
  331. $('#Up, #Down').on('click', adjustPasswordLength);
  332. // Bind to form events.
  333. $el.DisableTLD.on('change', toggleAlternateDomain);
  334. $el.DisableTLD.on('change', toggleTLDIndicator);
  335. $el.Inputs.on('keydown change', clearGeneratedPassword);
  336. $('#Passwd, #Secret, #MethodField').on('keyup change', generateIdenticon);
  337. // Bind to hotkeys.
  338. shortcut.add('Ctrl+O', toggleAdvancedOptions);
  339. shortcut.add('Ctrl+G', generatePassword);
  340. // Populate domain with referrer, if available.
  341. populateReferrer(document.referrer);
  342. // Set focus on password field.
  343. $el.Passwd.trigger('focus').trigger('change');
  344. // Attach postMessage listener for bookmarklet.
  345. $(window).on('message', listenForBookmarklet);