1# Copyright 2014 Google Inc. All Rights Reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Installs certificate on phone with KitKat.""" 16 17import argparse 18import logging 19import os 20import subprocess 21import sys 22 23KEYCODE_ENTER = '66' 24KEYCODE_TAB = '61' 25 26 27class CertInstallError(Exception): 28 pass 29 30 31class CertRemovalError(Exception): 32 pass 33 34 35 36_ANDROID_M_BUILD_VERSION = 23 37 38 39class AndroidCertInstaller(object): 40 """Certificate installer for phones with KitKat.""" 41 42 def __init__(self, device_id, cert_name, cert_path): 43 if not os.path.exists(cert_path): 44 raise ValueError('Not a valid certificate path') 45 self.device_id = device_id 46 self.cert_name = cert_name 47 self.cert_path = cert_path 48 self.file_name = os.path.basename(self.cert_path) 49 self.reformatted_cert_fname = None 50 self.reformatted_cert_path = None 51 self.android_cacerts_path = None 52 53 @staticmethod 54 def _run_cmd(cmd, dirname=None): 55 return subprocess.check_output(cmd, cwd=dirname) 56 57 def _adb(self, *args): 58 """Runs the adb command.""" 59 cmd = ['adb'] 60 if self.device_id: 61 cmd.extend(['-s', self.device_id]) 62 cmd.extend(args) 63 return self._run_cmd(cmd) 64 65 def _adb_shell(self, *args): 66 cmd = ['shell'] 67 cmd.extend(args) 68 return self._adb(*cmd) 69 70 def _adb_su_shell(self, *args): 71 """Runs command as root.""" 72 build_version_sdk = int(self._get_property('ro.build.version.sdk')) 73 if build_version_sdk >= _ANDROID_M_BUILD_VERSION: 74 cmd = ['su', '0'] 75 else: 76 cmd = ['su', '-c'] 77 cmd.extend(args) 78 return self._adb_shell(*cmd) 79 80 def _get_property(self, prop): 81 return self._adb_shell('getprop', prop).strip() 82 83 def check_device(self): 84 install_warning = False 85 if self._get_property('ro.product.device') != 'hammerhead': 86 logging.warning('Device is not hammerhead') 87 install_warning = True 88 if self._get_property('ro.build.version.release') != '4.4.2': 89 logging.warning('Version is not 4.4.2') 90 install_warning = True 91 if install_warning: 92 logging.warning('Certificate may not install properly') 93 94 def _input_key(self, key): 95 """Inputs a keyevent.""" 96 self._adb_shell('input', 'keyevent', key) 97 98 def _input_text(self, text): 99 """Inputs text.""" 100 self._adb_shell('input', 'text', text) 101 102 @staticmethod 103 def _remove(file_name): 104 """Deletes file.""" 105 if os.path.exists(file_name): 106 os.remove(file_name) 107 108 def _format_hashed_cert(self): 109 """Makes a certificate file that follows the format of files in cacerts.""" 110 self._remove(self.reformatted_cert_path) 111 contents = self._run_cmd(['openssl', 'x509', '-inform', 'PEM', '-text', 112 '-in', self.cert_path]) 113 description, begin_cert, cert_body = contents.rpartition('-----BEGIN ' 114 'CERTIFICATE') 115 contents = ''.join([begin_cert, cert_body, description]) 116 with open(self.reformatted_cert_path, 'w') as cert_file: 117 cert_file.write(contents) 118 119 def _remove_cert_from_cacerts(self): 120 self._adb_su_shell('mount', '-o', 'remount,rw', '/system') 121 self._adb_su_shell('rm', '-f', self.android_cacerts_path) 122 123 def _is_cert_installed(self): 124 return (self._adb_su_shell('ls', self.android_cacerts_path).strip() == 125 self.android_cacerts_path) 126 127 def _generate_reformatted_cert_path(self): 128 # Determine OpenSSL version, string is of the form 129 # 'OpenSSL 0.9.8za 5 Jun 2014' . 130 openssl_version = self._run_cmd(['openssl', 'version']).split() 131 132 if len(openssl_version) < 2: 133 raise ValueError('Unexpected OpenSSL version string: ', openssl_version) 134 135 # subject_hash flag name changed as of OpenSSL version 1.0.0 . 136 is_old_openssl_version = openssl_version[1].startswith('0') 137 subject_hash_flag = ( 138 '-subject_hash' if is_old_openssl_version else '-subject_hash_old') 139 140 output = self._run_cmd(['openssl', 'x509', '-inform', 'PEM', 141 subject_hash_flag, '-in', self.cert_path], 142 os.path.dirname(self.cert_path)) 143 self.reformatted_cert_fname = output.partition('\n')[0].strip() + '.0' 144 self.reformatted_cert_path = os.path.join(os.path.dirname(self.cert_path), 145 self.reformatted_cert_fname) 146 self.android_cacerts_path = ('/system/etc/security/cacerts/%s' % 147 self.reformatted_cert_fname) 148 149 def remove_cert(self): 150 self._generate_reformatted_cert_path() 151 152 if self._is_cert_installed(): 153 self._remove_cert_from_cacerts() 154 155 if self._is_cert_installed(): 156 raise CertRemovalError('Cert Removal Failed') 157 158 def install_cert(self, overwrite_cert=False): 159 """Installs a certificate putting it in /system/etc/security/cacerts.""" 160 self._generate_reformatted_cert_path() 161 162 if self._is_cert_installed(): 163 if overwrite_cert: 164 self._remove_cert_from_cacerts() 165 else: 166 logging.info('cert is already installed') 167 return 168 169 self._format_hashed_cert() 170 self._adb('push', self.reformatted_cert_path, '/sdcard/') 171 self._remove(self.reformatted_cert_path) 172 self._adb_su_shell('mount', '-o', 'remount,rw', '/system') 173 self._adb_su_shell( 174 'cp', '/sdcard/%s' % self.reformatted_cert_fname, 175 '/system/etc/security/cacerts/%s' % self.reformatted_cert_fname) 176 self._adb_su_shell('chmod', '644', self.android_cacerts_path) 177 if not self._is_cert_installed(): 178 raise CertInstallError('Cert Install Failed') 179 180 def install_cert_using_gui(self): 181 """Installs certificate on the device using adb commands.""" 182 self.check_device() 183 # TODO(mruthven): Add a check to see if the certificate is already installed 184 # Install the certificate. 185 logging.info('Installing %s on %s', self.cert_path, self.device_id) 186 self._adb('push', self.cert_path, '/sdcard/') 187 188 # Start credential install intent. 189 self._adb_shell('am', 'start', '-W', '-a', 'android.credentials.INSTALL') 190 191 # Move to and click search button. 192 self._input_key(KEYCODE_TAB) 193 self._input_key(KEYCODE_TAB) 194 self._input_key(KEYCODE_ENTER) 195 196 # Search for certificate and click it. 197 # Search only works with lower case letters 198 self._input_text(self.file_name.lower()) 199 self._input_key(KEYCODE_ENTER) 200 201 # These coordinates work for hammerhead devices. 202 self._adb_shell('input', 'tap', '300', '300') 203 204 # Name the certificate and click enter. 205 self._input_text(self.cert_name) 206 self._input_key(KEYCODE_TAB) 207 self._input_key(KEYCODE_TAB) 208 self._input_key(KEYCODE_TAB) 209 self._input_key(KEYCODE_ENTER) 210 211 # Remove the file. 212 self._adb_shell('rm', '/sdcard/' + self.file_name) 213 214 215def parse_args(): 216 """Parses command line arguments.""" 217 parser = argparse.ArgumentParser(description='Install cert on device.') 218 parser.add_argument( 219 '-n', '--cert-name', default='dummycert', help='certificate name') 220 parser.add_argument( 221 '--overwrite', default=False, action='store_true', 222 help='Overwrite certificate file if it is already installed') 223 parser.add_argument( 224 '--remove', default=False, action='store_true', 225 help='Remove certificate file if it is installed') 226 parser.add_argument( 227 '--device-id', help='device serial number') 228 parser.add_argument( 229 'cert_path', help='Certificate file path') 230 return parser.parse_args() 231 232 233def main(): 234 args = parse_args() 235 cert_installer = AndroidCertInstaller(args.device_id, args.cert_name, 236 args.cert_path) 237 if args.remove: 238 cert_installer.remove_cert() 239 else: 240 cert_installer.install_cert(args.overwrite) 241 242 243if __name__ == '__main__': 244 sys.exit(main()) 245