1#  Copyright (C) 2020 The Android Open Source Project
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#  Licensed under the Apache License, Version 2.0 (the "License");
16#  you may not use this file except in compliance with the License.
17#  You may obtain a copy of the License at
18#
19#       http://www.apache.org/licenses/LICENSE-2.0
20#
21#  Unless required by applicable law or agreed to in writing, software
22#  distributed under the License is distributed on an "AS IS" BASIS,
23#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
24#  See the License for the specific language governing permissions and
25#  limitations under the License.
26"""Repacking tool for Shared Libs APEX testing."""
27
28import argparse
29import hashlib
30import logging
31import os
32import shutil
33import subprocess
34import sys
35import tempfile
36from zipfile import ZipFile
37
38import apex_build_info_pb2
39import apex_manifest_pb2
40
41logger = logging.getLogger(__name__)
42
43def comma_separated_list(arg):
44  return arg.split(',')
45
46
47def parse_args(argv):
48  parser = argparse.ArgumentParser(
49      description='Repacking tool for Shared Libs APEX testing')
50
51  parser.add_argument('--input', required=True, help='Input file')
52  parser.add_argument('--output', required=True, help='Output file')
53  parser.add_argument(
54      '--key', required=True, help='Path to the private avb key file')
55  parser.add_argument(
56      '--pk8key',
57      required=True,
58      help='Path to the private apk key file in pk8 format')
59  parser.add_argument(
60      '--pubkey', required=True, help='Path to the public avb key file')
61  parser.add_argument(
62      '--tmpdir', required=True, help='Temporary directory to use')
63  parser.add_argument(
64      '--x509key',
65      required=True,
66      help='Path to the public apk key file in x509 format')
67  parser.add_argument(
68      '--mode', default='strip', choices=['strip', 'sharedlibs'])
69  parser.add_argument(
70      '--libs',
71      default='libc++.so,libsharedlibtest.so',
72      type=comma_separated_list,
73      help='Libraries to strip/repack. Expects comma separated values.')
74  return parser.parse_args(argv)
75
76
77def run(args, verbose=None, **kwargs):
78  """Creates and returns a subprocess.Popen object.
79
80  Args:
81    args: The command represented as a list of strings.
82    verbose: Whether the commands should be shown. Default to the global
83      verbosity if unspecified.
84    kwargs: Any additional args to be passed to subprocess.Popen(), such as env,
85      stdin, etc. stdout and stderr will default to subprocess.PIPE and
86      subprocess.STDOUT respectively unless caller specifies any of them.
87      universal_newlines will default to True, as most of the users in
88      releasetools expect string output.
89
90  Returns:
91    A subprocess.Popen object.
92  """
93  if 'stdout' not in kwargs and 'stderr' not in kwargs:
94    kwargs['stdout'] = subprocess.PIPE
95    kwargs['stderr'] = subprocess.STDOUT
96  if 'universal_newlines' not in kwargs:
97    kwargs['universal_newlines'] = True
98  if verbose:
99    logger.info('  Running: \"%s\"', ' '.join(args))
100  return subprocess.Popen(args, **kwargs)
101
102
103def run_and_check_output(args, verbose=None, **kwargs):
104  """Runs the given command and returns the output.
105
106  Args:
107    args: The command represented as a list of strings.
108    verbose: Whether the commands should be shown. Default to the global
109      verbosity if unspecified.
110    kwargs: Any additional args to be passed to subprocess.Popen(), such as env,
111      stdin, etc. stdout and stderr will default to subprocess.PIPE and
112      subprocess.STDOUT respectively unless caller specifies any of them.
113
114  Returns:
115    The output string.
116
117  Raises:
118    ExternalError: On non-zero exit from the command.
119  """
120  proc = run(args, verbose=verbose, **kwargs)
121  output, _ = proc.communicate()
122  if output is None:
123    output = ''
124  # Don't log any if caller explicitly says so.
125  if verbose:
126    logger.info('%s', output.rstrip())
127  if proc.returncode != 0:
128    raise RuntimeError(
129        'Failed to run command \'{}\' (exit code {}):\n{}'.format(
130            args, proc.returncode, output))
131  return output
132
133
134def get_container_files(apex_file_path, tmpdir):
135  dir_name = tempfile.mkdtemp(prefix='container_files_', dir=tmpdir)
136  with ZipFile(apex_file_path, 'r') as zip_obj:
137    zip_obj.extractall(path=dir_name)
138  files = {}
139  for i in [
140      'apex_manifest.json', 'apex_manifest.pb', 'apex_build_info.pb', 'assets',
141      'apex_payload.img', 'apex_payload.zip'
142  ]:
143    file_path = os.path.join(dir_name, i)
144    if os.path.exists(file_path):
145      files[i] = file_path
146
147  image_file = files.get('apex_payload.img')
148  if image_file is None:
149    image_file = files.get('apex_payload.zip')
150
151  files['apex_payload'] = image_file
152
153  return files
154
155
156def extract_payload_from_img(img_file_path, tmpdir):
157  dir_name = tempfile.mkdtemp(prefix='extracted_payload_', dir=tmpdir)
158  cmd = [
159      _get_host_tools_path('debugfs_static'), '-R',
160      'rdump ./ %s' % dir_name, img_file_path
161  ]
162  run_and_check_output(cmd)
163
164  # Remove payload files added by apexer and e2fs tools.
165  for i in ['apex_manifest.json', 'apex_manifest.pb']:
166    if os.path.exists(os.path.join(dir_name, i)):
167      os.remove(os.path.join(dir_name, i))
168  if os.path.isdir(os.path.join(dir_name, 'lost+found')):
169    shutil.rmtree(os.path.join(dir_name, 'lost+found'))
170  return dir_name
171
172
173def run_apexer(container_files, payload_dir, key_path, pubkey_path, tmpdir):
174  apexer_cmd = _get_host_tools_path('apexer')
175  cmd = [
176      apexer_cmd, '--force', '--include_build_info', '--do_not_check_keyname'
177  ]
178  cmd.extend([
179      '--apexer_tool_path',
180      os.path.dirname(apexer_cmd) + ':prebuilts/sdk/tools/linux/bin'
181  ])
182  cmd.extend(['--manifest', container_files['apex_manifest.pb']])
183  if 'apex_manifest.json' in container_files:
184    cmd.extend(['--manifest_json', container_files['apex_manifest.json']])
185  cmd.extend(['--build_info', container_files['apex_build_info.pb']])
186  if 'assets' in container_files:
187    cmd.extend(['--assets_dir', container_files['assets']])
188  cmd.extend(['--key', key_path])
189  cmd.extend(['--pubkey', pubkey_path])
190
191  # Decide on output file name
192  apex_suffix = '.apex.unsigned'
193  fd, fn = tempfile.mkstemp(prefix='repacked_', suffix=apex_suffix, dir=tmpdir)
194  os.close(fd)
195  cmd.extend([payload_dir, fn])
196
197  run_and_check_output(cmd)
198  return fn
199
200
201def _get_java_toolchain():
202  java_toolchain = 'java'
203  if os.path.isfile('prebuilts/jdk/jdk11/linux-x86/bin/java'):
204    java_toolchain = 'prebuilts/jdk/jdk11/linux-x86/bin/java'
205
206  java_dep_lib = (
207      os.path.join(os.path.dirname(_get_host_tools_path()), 'lib64') + ':' +
208      os.path.join(os.path.dirname(_get_host_tools_path()), 'lib'))
209
210  return [java_toolchain, java_dep_lib]
211
212
213def _get_host_tools_path(tool_name=None):
214  # This script is located at e.g.
215  # out/soong/host/linux-x86/bin/shared_libs_repack/shared_libs_repack.py.
216  # Find the host tools dir by going up two directories.
217  dirname = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
218  if tool_name:
219    return os.path.join(dirname, tool_name)
220  return dirname
221
222
223def sign_apk_container(unsigned_apex, x509key_path, pk8key_path, tmpdir):
224  fd, fn = tempfile.mkstemp(prefix='repacked_', suffix='.apex', dir=tmpdir)
225  os.close(fd)
226  java_toolchain, java_dep_lib = _get_java_toolchain()
227
228  cmd = [
229      java_toolchain, '-Djava.library.path=' + java_dep_lib, '-jar',
230      os.path.join(
231          os.path.dirname(_get_host_tools_path()), 'framework', 'signapk.jar'),
232      '-a', '4096', x509key_path, pk8key_path, unsigned_apex, fn
233  ]
234  run_and_check_output(cmd)
235  return fn
236
237
238def compute_sha512(file_path):
239  block_size = 65536
240  hashbuf = hashlib.sha512()
241  with open(file_path, 'rb') as f:
242    fb = f.read(block_size)
243    while len(fb) > 0:
244      hashbuf.update(fb)
245      fb = f.read(block_size)
246  return hashbuf.hexdigest()
247
248
249def parse_fs_config(fs_config):
250  configs = fs_config.splitlines()
251  # Result is set of configurations.
252  # Each configuration is set of items as [file path, uid, gid, mode].
253  # All items are stored as string.
254  result = []
255  for config in configs:
256    result.append(config.split())
257  return result
258
259
260def config_to_str(configs):
261  result = ''
262  for config in configs:
263    result += ' '.join(config) + '\n'
264  return result
265
266
267def _extract_lib_or_lib64(payload_dir, lib_full_path):
268  # Figure out if this is lib or lib64:
269  # Strip out the payload_dir and split by /
270  libpath = lib_full_path[len(payload_dir):].lstrip('/').split('/')
271  return libpath[0]
272
273
274def main(argv):
275  args = parse_args(argv)
276  apex_file_path = args.input
277
278  container_files = get_container_files(apex_file_path, args.tmpdir)
279  payload_dir = extract_payload_from_img(container_files['apex_payload.img'],
280                                         args.tmpdir)
281  libs = args.libs
282  assert len(libs)> 0
283
284  lib_paths = [os.path.join(payload_dir, lib_dir, lib)
285               for lib_dir in ['lib', 'lib64']
286               for lib in libs
287               if os.path.exists(os.path.join(payload_dir, lib_dir, lib))]
288
289  assert len(lib_paths) > 0
290
291  lib_paths_hashes = [(lib, compute_sha512(lib)) for lib in lib_paths]
292
293  if args.mode == 'strip':
294    # Stripping mode. Add a reference to the version of libc++.so to the
295    # requireSharedApexLibs entry in the manifest, and remove lib64/libc++.so
296    # from the payload.
297    pb = apex_manifest_pb2.ApexManifest()
298    with open(container_files['apex_manifest.pb'], 'rb') as f:
299      pb.ParseFromString(f.read())
300      for lib_path_hash in lib_paths_hashes:
301        basename = os.path.basename(lib_path_hash[0])
302        libpath = _extract_lib_or_lib64(payload_dir, lib_path_hash[0])
303        assert libpath in ('lib', 'lib64')
304        pb.requireSharedApexLibs.append(os.path.join(libpath, basename) + ':'
305                                        + lib_path_hash[1])
306        # Replace existing library with symlink
307        symlink_dst = os.path.join('/', 'apex', 'sharedlibs',
308                                   libpath, basename, lib_path_hash[1],
309                                   basename)
310        os.remove(lib_path_hash[0])
311        os.system('ln -s {0} {1}'.format(symlink_dst, lib_path_hash[0]))
312      #
313      # Example of resulting manifest:
314      # ---
315      # name: "com.android.apex.test.foo"
316      # version: 1
317      # requireNativeLibs: "libc.so"
318      # requireNativeLibs: "libdl.so"
319      # requireNativeLibs: "libm.so"
320      # requireSharedApexLibs: "lib/libc++.so:23c5dd..."
321      # requireSharedApexLibs: "lib/libsharedlibtest.so:870f38..."
322      # requireSharedApexLibs: "lib64/libc++.so:72a584..."
323      # requireSharedApexLibs: "lib64/libsharedlibtest.so:109015..."
324      # --
325      # To print uncomment the following:
326      # from google.protobuf import text_format
327      # print(text_format.MessageToString(pb))
328    with open(container_files['apex_manifest.pb'], 'wb') as f:
329      f.write(pb.SerializeToString())
330
331  if args.mode == 'sharedlibs':
332    # Sharedlibs mode. Mark in the APEX manifest that this package contains
333    # shared libraries.
334    pb = apex_manifest_pb2.ApexManifest()
335    with open(container_files['apex_manifest.pb'], 'rb') as f:
336      pb.ParseFromString(f.read())
337      del pb.requireNativeLibs[:]
338      pb.provideSharedApexLibs = True
339    with open(container_files['apex_manifest.pb'], 'wb') as f:
340      f.write(pb.SerializeToString())
341
342    pb = apex_build_info_pb2.ApexBuildInfo()
343    with open(container_files['apex_build_info.pb'], 'rb') as f:
344      pb.ParseFromString(f.read())
345
346    canned_fs_config = parse_fs_config(pb.canned_fs_config.decode('utf-8'))
347
348    # Remove the bin directory from payload dir and from the canned_fs_config.
349    shutil.rmtree(os.path.join(payload_dir, 'bin'))
350    canned_fs_config = [config for config in canned_fs_config
351                        if not config[0].startswith('/bin')]
352
353    # Remove from the canned_fs_config the entries we are about to relocate in
354    # different dirs.
355    source_lib_paths = [os.path.join('/', libpath, lib)
356                        for libpath in ['lib', 'lib64']
357                        for lib in libs]
358    # We backup the fs config lines for the libraries we are going to relocate,
359    # so we can set the same permissions later.
360    canned_fs_config_original_lib = {config[0] : config
361                                     for config in canned_fs_config
362                                     if config[0] in source_lib_paths}
363
364    canned_fs_config = [config for config in canned_fs_config
365                        if config[0] not in source_lib_paths]
366
367    # We move any targeted library in lib64/ or lib/ to a directory named
368    # /lib64/libNAME.so/${SHA512_OF_LIBCPP}/ or
369    # /lib/libNAME.so/${SHA512_OF_LIBCPP}/
370    #
371    for lib_path_hash in lib_paths_hashes:
372      basename = os.path.basename(lib_path_hash[0])
373      libpath = _extract_lib_or_lib64(payload_dir, lib_path_hash[0])
374      tmp_lib = os.path.join(payload_dir, libpath, basename + '.bak')
375      shutil.move(lib_path_hash[0], tmp_lib)
376      destdir = os.path.join(payload_dir, libpath, basename, lib_path_hash[1])
377      os.makedirs(destdir)
378      shutil.move(tmp_lib, os.path.join(destdir, basename))
379
380      canned_fs_config.append(
381          ['/' + libpath + '/' + basename, '0', '2000', '0755'])
382      canned_fs_config.append(
383          ['/' + libpath + '/' + basename + '/' + lib_path_hash[1],
384           '0', '2000', '0755'])
385
386      if os.path.join('/', libpath, basename) in canned_fs_config_original_lib:
387        config = canned_fs_config_original_lib[os.path.join(
388                                                   '/',
389                                                   libpath,
390                                                   basename)]
391        canned_fs_config.append([os.path.join('/', libpath, basename,
392                                              lib_path_hash[1], basename),
393                                config[1], config[2], config[3]])
394      else:
395        canned_fs_config.append([os.path.join('/', libpath, basename,
396                                              lib_path_hash[1], basename),
397                                '1000', '1000', '0644'])
398
399    pb.canned_fs_config = config_to_str(canned_fs_config).encode('utf-8')
400    with open(container_files['apex_build_info.pb'], 'wb') as f:
401      f.write(pb.SerializeToString())
402
403  try:
404    for lib in lib_paths:
405      os.rmdir(os.path.dirname(lib))
406  except OSError:
407    # Directory not empty, that's OK.
408    pass
409
410  repack_apex_file_path = run_apexer(container_files, payload_dir, args.key,
411                                     args.pubkey, args.tmpdir)
412
413  resigned_apex_file_path = sign_apk_container(repack_apex_file_path,
414                                               args.x509key, args.pk8key,
415                                               args.tmpdir)
416
417  shutil.copyfile(resigned_apex_file_path, args.output)
418
419
420if __name__ == '__main__':
421  main(sys.argv[1:])
422