Update appcache version.
[skeletonkey.git] / core.js
1 /* Copyright (c) 2012 Robert Sesek <http://robert.sesek.com>
2 *
3 * Permission is hereby granted, free of charge, to any person obtaining a copy
4 * of this software and associated documentation files (the "Software"), to
5 * deal in the Software without restriction, including without limitation the
6 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7 * sell copies of the Software, and to permit persons to whom the Software is
8 * furnished to do so, subject to the following conditions:
9 *
10 * The above copyright notice and this permission notice shall be included in
11 * all copies or substantial portions of the Software.
12 *
13 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 * DEALINGS IN THE SOFTWARE.
20 */
21
22 (function main() {
23 document.addEventListener('DOMContentLoaded', function() {
24 var controller = new SkeletonKey(document);
25 });
26 })();
27
28 /**
29 * SkeletonKey is view controller for generating secure passwords.
30 *
31 * @param {HTMLDocument} doc The document on which to operate.
32 */
33 var SkeletonKey = SkeletonKey || function(doc) {
34 this._master = doc.getElementById('master');
35 this._sitekey = doc.getElementById('sitekey');
36 this._username = doc.getElementById('username');
37 this._password = doc.getElementById('password');
38 this._generateButton = doc.getElementById('generate');
39
40 // If this is an extension, use defaults until the Chrome settings are loaded.
41 var win = null;
42 if (!this._isChromeExtension())
43 win = window;
44 this._options = new SkeletonKeyOptions(null, win);
45
46
47 this._init();
48 };
49
50 /**
51 * The number of iterations to perform in PBKDF2.
52 * @const {int}
53 */
54 SkeletonKey.prototype.ITERATIONS = 1000;
55 /**
56 * The size of the key, in bytes.
57 * @const {int}
58 */
59 SkeletonKey.prototype.KEYSIZE = 256/32;
60
61 /**
62 * Initializes event handlers for the page.
63 * @private
64 */
65 SkeletonKey.prototype._init = function() {
66 this._generateButton.onclick = this._onGenerate.bind(this);
67
68 this._master.onkeyup = this._nextFieldInterceptor.bind(this);
69 this._sitekey.onkeyup = this._nextFieldInterceptor.bind(this);
70 this._username.onkeyup = this._nextFieldInterceptor.bind(this);
71
72 this._password.onmousedown = this._selectPassword.bind(this);
73 this._password.labels[0].onmousedown = this._selectPassword.bind(this);
74
75 function eatEvent(e) {
76 e.stopPropagation();
77 e.preventDefault();
78 }
79 this._password.onmouseup = eatEvent;
80 this._password.labels[0].onmouseup = eatEvent;
81
82 if (this._isChromeExtension()) {
83 this._initChromeExtension();
84 } else {
85 // Chrome extensions will get the first field focused automatically, so only
86 // do it explicitly for hosted pages.
87 this._master.focus();
88 }
89 };
90
91 /**
92 * Event handler for generating a new password.
93 * @param {Event} e
94 * @private
95 */
96 SkeletonKey.prototype._onGenerate = function(e) {
97 var salt = this._username.value + '@' + this._sitekey.value;
98
99 // |key| is a WordArray of 32-bit words.
100 var key = CryptoJS.PBKDF2(this._master.value, salt,
101 {keySize: this.KEYSIZE, iterations: this.ITERATIONS});
102
103 var hexString = key.toString();
104 hexString = this._capitalizeKey(hexString);
105
106 var maxLength = this._options.getMaximumPasswordLength();
107 if (hexString.length > maxLength)
108 hexString = hexString.substr(0, maxLength);
109
110 this._password.value = hexString;
111 this._selectPassword();
112 };
113
114 /**
115 * Takes a HEX string and returns a mixed-case string.
116 * @param {string} key
117 * @return string
118 * @private
119 */
120 SkeletonKey.prototype._capitalizeKey = function(key) {
121 // |key| is too long for a decent password, so try and use the second half of
122 // it as the basis for capitalizing the key.
123 var capsSource = null;
124 var keyLength = key.length;
125 if (keyLength / 2 <= this._options.getMinimumPasswordLength()) {
126 capsSouce = key.substr(0, keyLength - this._options.getMinimumPasswordLength());
127 } else {
128 capsSource = key.substr(keyLength / 2);
129 }
130
131 if (!capsSource || capsSource.length < 1) {
132 return key;
133 }
134
135 key = key.substr(0, capsSource.length);
136 var capsSourceLength = capsSource.length;
137
138 var j = 0;
139 var newKey = "";
140 for (var i = 0; i < key.length; i++) {
141 var c = key.charCodeAt(i);
142 // If this is not a lowercase letter or there's no more source, skip.
143 if (c < 0x61 || c > 0x7A || j >= capsSourceLength) {
144 newKey += key[i];
145 continue;
146 }
147
148 var makeCap = capsSource.charCodeAt(j++) % 2;
149 if (makeCap)
150 newKey += String.fromCharCode(c - 0x20);
151 else
152 newKey += key[i];
153 }
154
155 return newKey;
156 };
157
158 /**
159 * Checks if the given key event is from the enter key and moves onto the next
160 * field or generates the password.
161 * @param {Event} e
162 * @private
163 */
164 SkeletonKey.prototype._nextFieldInterceptor = function(e) {
165 if (e.keyCode != 0xD)
166 return;
167
168 if (this._master.value == "") {
169 this._master.focus();
170 } else if (this._sitekey.value == "") {
171 this._sitekey.focus();
172 } else if (this._username.value == "") {
173 this._username.focus();
174 } else {
175 this._generateButton.click();
176 }
177 };
178
179 /**
180 * Selects the contents of the generated password.
181 * @private
182 */
183 SkeletonKey.prototype._selectPassword = function() {
184 this._password.focus();
185 this._password.select();
186 };
187
188 /**
189 * Initalizes the Chrome extension pieces if running inside chrome.
190 * @private
191 */
192 SkeletonKey.prototype._initChromeExtension = function() {
193 var query = {
194 "active": true,
195 "currentWindow": true
196 };
197 chrome.tabs.query(query, function (tabs) {
198 console.log(tabs);
199 if (tabs == null || tabs.length != 1)
200 return;
201
202 var url = tabs[0].url;
203 if (url == null || url == "")
204 return;
205
206 // Use a link to clevely parse the URL into the hostname.
207 var parser = document.createElement("a");
208 parser.href = url;
209 var hostname = parser.hostname.split(".");
210
211 // Filter out common subdomains and TLDs to keep the siteky short and
212 // memorable.
213 ["www", "login", "account", "accounts"].forEach(function(subdomain) {
214 if (hostname[0] == subdomain) {
215 hostname.shift();
216 return;
217 }
218 });
219
220 ["com", "net", "org", "edu", "info"].forEach(function(tld) {
221 if (hostname[hostname.length - 1] == tld) {
222 hostname.pop();
223 return;
224 }
225 });
226
227 this._sitekey.value = hostname.join(".");
228 }.bind(this));
229 };
230
231 /**
232 * Checks if SkeletonKey is running as a Chrome extension.
233 * @returns {bool}
234 * @private
235 */
236 SkeletonKey.prototype._isChromeExtension = function() {
237 return typeof chrome !== 'undefined' && typeof chrome.tabs !== 'undefined';
238 };