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 17 import argparse 18 import logging 19 import os 20 import subprocess 21 import sys 22 23 KEYCODE_ENTER = '66' 24 KEYCODE_TAB = '61' 25 26 27 class CertInstallError(Exception): 28 pass 29 30 31 class CertRemovalError(Exception): 32 pass 33 34 35 36 _ANDROID_M_BUILD_VERSION = 23 37 38 39 class 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 215 def 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 233 def 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 243 if __name__ == '__main__': 244 sys.exit(main()) 245