1 /* Copyright (c) 2012 Robert Sesek <http://robert.sesek.com>
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:
10 * The above copyright notice and this permission notice shall be included in
11 * all copies or substantial portions of the Software.
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.
23 document
.addEventListener('DOMContentLoaded', function() {
24 var controller
= new SkeletonKey(document
);
29 * SkeletonKey is view controller for generating secure passwords.
31 * @param {HTMLDocument} doc The document on which to operate.
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');
40 // If this is an extension, use defaults until the Chrome settings are loaded.
42 if (!this._isChromeExtension())
44 this._options
= new SkeletonKeyOptions(null, win
);
51 * The number of iterations to perform in PBKDF2.
54 SkeletonKey
.prototype.ITERATIONS
= 1000;
56 * The size of the key, in bytes.
59 SkeletonKey
.prototype.KEYSIZE
= 256/32;
62 * Initializes event handlers for the page.
65 SkeletonKey
.prototype._init
= function() {
66 this._generateButton
.onclick
= this._onGenerate
.bind(this);
68 this._master
.onkeyup
= this._nextFieldInterceptor
.bind(this);
69 this._sitekey
.onkeyup
= this._nextFieldInterceptor
.bind(this);
70 this._username
.onkeyup
= this._nextFieldInterceptor
.bind(this);
72 if (!this._isTouchDevice()) {
73 this._password
.onmousedown
= this._selectPassword
.bind(this);
74 this._password
.onmouseup
= function(e
) {
80 if (this._isChromeExtension()) {
81 this._initChromeExtension();
83 // Chrome extensions will get the first field focused automatically, so only
84 // do it explicitly for hosted pages.
90 * Event handler for generating a new password.
94 SkeletonKey
.prototype._onGenerate
= function(e
) {
95 var salt
= this._username
.value
+ '@' + this._sitekey
.value
;
97 // |key| is a WordArray of 32-bit words.
98 var key
= CryptoJS
.PBKDF2(this._master
.value
, salt
,
99 {keySize
: this.KEYSIZE
, iterations
: this.ITERATIONS
});
101 var hexString
= key
.toString();
102 hexString
= this._capitalizeKey(hexString
);
104 var maxLength
= this._options
.getMaximumPasswordLength();
105 if (hexString
.length
> maxLength
)
106 hexString
= hexString
.substr(0, maxLength
);
108 this._password
.innerText
= hexString
;
109 this._selectPassword();
113 * Takes a HEX string and returns a mixed-case string.
114 * @param {string} key
118 SkeletonKey
.prototype._capitalizeKey
= function(key
) {
119 // |key| is too long for a decent password, so try and use the second half of
120 // it as the basis for capitalizing the key.
121 var capsSource
= null;
122 var keyLength
= key
.length
;
123 if (keyLength
/ 2 <= this._options
.getMinimumPasswordLength()) {
124 capsSouce
= key
.substr(0, keyLength
- this._options
.getMinimumPasswordLength());
126 capsSource
= key
.substr(keyLength
/ 2);
129 if (!capsSource
|| capsSource
.length
< 1) {
133 key
= key
.substr(0, capsSource
.length
);
134 var capsSourceLength
= capsSource
.length
;
138 for (var i
= 0; i
< key
.length
; i
++) {
139 var c
= key
.charCodeAt(i
);
140 // If this is not a lowercase letter or there's no more source, skip.
141 if (c
< 0x61 || c
> 0x7A || j
>= capsSourceLength
) {
146 var makeCap
= capsSource
.charCodeAt(j
++) % 2;
148 newKey
+= String
.fromCharCode(c
- 0x20);
157 * Checks if the given key event is from the enter key and moves onto the next
158 * field or generates the password.
162 SkeletonKey
.prototype._nextFieldInterceptor
= function(e
) {
163 if (e
.keyCode
!= 0xD)
166 if (this._master
.value
== "") {
167 this._master
.focus();
168 } else if (this._sitekey
.value
== "") {
169 this._sitekey
.focus();
170 } else if (this._username
.value
== "") {
171 this._username
.focus();
173 this._generateButton
.click();
178 * Selects the contents of the generated password.
181 SkeletonKey
.prototype._selectPassword
= function() {
182 this._generateButton
.blur();
184 // Touch devices do not bring up the edit controls (for copy) for
185 // pre-selected text.
186 if (this._isTouchDevice())
189 var range
= document
.createRange();
190 range
.selectNode(this._password
.firstChild
); // Select #text node.
192 var selection
= window
.getSelection();
193 selection
.removeAllRanges();
194 selection
.addRange(range
);
198 * Initalizes the Chrome extension pieces if running inside chrome.
201 SkeletonKey
.prototype._initChromeExtension
= function() {
204 "currentWindow": true
206 chrome
.tabs
.query(query
, function (tabs
) {
208 if (tabs
== null || tabs
.length
!= 1)
211 var url
= tabs
[0].url
;
212 if (url
== null || url
== "")
215 // Use a link to clevely parse the URL into the hostname.
216 var parser
= document
.createElement("a");
218 var hostname
= parser
.hostname
.split(".");
220 // Filter out common subdomains and TLDs to keep the siteky short and
222 ["www", "login", "account", "accounts"].forEach(function(subdomain
) {
223 if (hostname
[0] == subdomain
) {
229 ["com", "net", "org", "edu", "info"].forEach(function(tld
) {
230 if (hostname
[hostname
.length
- 1] == tld
) {
236 this._sitekey
.value
= hostname
.join(".");
241 * Checks if SkeletonKey is running as a Chrome extension.
245 SkeletonKey
.prototype._isChromeExtension
= function() {
246 return typeof chrome
!== 'undefined' && typeof chrome
.tabs
!== 'undefined';
250 * Checks if SkeletonKey is running on a touch device.
254 SkeletonKey
.prototype._isTouchDevice
= function() {
255 return typeof document
.createTouch
=== 'function';