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