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