1 // Copyright 2013 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 /** 6 * @fileoverview 7 * Script to be injected into SAML provider pages, serving three main purposes: 8 * 1. Signal hosting extension that an external page is loaded so that the 9 * UI around it should be changed accordingly; 10 * 2. Provide an API via which the SAML provider can pass user credentials to 11 * Chrome OS, allowing the password to be used for encrypting user data and 12 * offline login. 13 * 3. Scrape password fields, making the password available to Chrome OS even if 14 * the SAML provider does not support the credential passing API. 15 */ 16 17 (function() { 18 function APICallForwarder() { 19 } 20 21 /** 22 * The credential passing API is used by sending messages to the SAML page's 23 * |window| object. This class forwards API calls from the SAML page to a 24 * background script and API responses from the background script to the SAML 25 * page. Communication with the background script occurs via a |Channel|. 26 */ 27 APICallForwarder.prototype = { 28 // Channel to which API calls are forwarded. 29 channel_: null, 30 31 /** 32 * Initialize the API call forwarder. 33 * @param {!Object} channel Channel to which API calls should be forwarded. 34 */ 35 init: function(channel) { 36 this.channel_ = channel; 37 this.channel_.registerMessage('apiResponse', 38 this.onAPIResponse_.bind(this)); 39 40 window.addEventListener('message', this.onMessage_.bind(this)); 41 }, 42 43 onMessage_: function(event) { 44 if (event.source != window || 45 typeof event.data != 'object' || 46 !event.data.hasOwnProperty('type') || 47 event.data.type != 'gaia_saml_api') { 48 return; 49 } 50 // Forward API calls to the background script. 51 this.channel_.send({name: 'apiCall', call: event.data.call}); 52 }, 53 54 onAPIResponse_: function(msg) { 55 // Forward API responses to the SAML page. 56 window.postMessage({type: 'gaia_saml_api_reply', response: msg.response}, 57 '/'); 58 } 59 }; 60 61 /** 62 * A class to scrape password from type=password input elements under a given 63 * docRoot and send them back via a Channel. 64 */ 65 function PasswordInputScraper() { 66 } 67 68 PasswordInputScraper.prototype = { 69 // URL of the page. 70 pageURL_: null, 71 72 // Channel to send back changed password. 73 channel_: null, 74 75 // An array to hold password fields. 76 passwordFields_: null, 77 78 // An array to hold cached password values. 79 passwordValues_: null, 80 81 /** 82 * Initialize the scraper with given channel and docRoot. Note that the 83 * scanning for password fields happens inside the function and does not 84 * handle DOM tree changes after the call returns. 85 * @param {!Object} channel The channel to send back password. 86 * @param {!string} pageURL URL of the page. 87 * @param {!HTMLElement} docRoot The root element of the DOM tree that 88 * contains the password fields of interest. 89 */ 90 init: function(channel, pageURL, docRoot) { 91 this.pageURL_ = pageURL; 92 this.channel_ = channel; 93 94 this.passwordFields_ = docRoot.querySelectorAll('input[type=password]'); 95 this.passwordValues_ = []; 96 97 for (var i = 0; i < this.passwordFields_.length; ++i) { 98 this.passwordFields_[i].addEventListener( 99 'input', this.onPasswordChanged_.bind(this, i)); 100 101 this.passwordValues_[i] = this.passwordFields_[i].value; 102 } 103 }, 104 105 /** 106 * Check if the password field at |index| has changed. If so, sends back 107 * the updated value. 108 */ 109 maybeSendUpdatedPassword: function(index) { 110 var newValue = this.passwordFields_[index].value; 111 if (newValue == this.passwordValues_[index]) 112 return; 113 114 this.passwordValues_[index] = newValue; 115 116 // Use an invalid char for URL as delimiter to concatenate page url and 117 // password field index to construct a unique ID for the password field. 118 var passwordId = this.pageURL_ + '|' + index; 119 this.channel_.send({ 120 name: 'updatePassword', 121 id: passwordId, 122 password: newValue 123 }); 124 }, 125 126 /** 127 * Handles 'change' event in the scraped password fields. 128 * @param {number} index The index of the password fields in 129 * |passwordFields_|. 130 */ 131 onPasswordChanged_: function(index) { 132 this.maybeSendUpdatedPassword(index); 133 } 134 }; 135 136 /** 137 * Heuristic test whether the current page is a relevant SAML page. 138 * Current implementation checks if it is a http or https page and has 139 * some content in it. 140 * @return {boolean} Whether the current page looks like a SAML page. 141 */ 142 function isSAMLPage() { 143 var url = window.location.href; 144 if (!url.match(/^(http|https):\/\//)) 145 return false; 146 147 return document.body.scrollWidth > 50 && document.body.scrollHeight > 50; 148 } 149 150 if (isSAMLPage()) { 151 var pageURL = window.location.href; 152 153 var channel = new Channel(); 154 channel.connect('injected'); 155 channel.send({name: 'pageLoaded', url: pageURL}); 156 157 var apiCallForwarder = new APICallForwarder(); 158 apiCallForwarder.init(channel); 159 160 var passwordScraper = new PasswordInputScraper(); 161 passwordScraper.init(channel, pageURL, document.documentElement); 162 } 163 })(); 164