# -*- coding: utf-8 -*-
# Trueseeing: Non-decompiling Android application vulnerability scanner
# Copyright (C) 2017-23 Takahiro Yoshimura <altakey@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from __future__ import annotations
from typing import TYPE_CHECKING

import os

if TYPE_CHECKING:
  from typing import List
  from trueseeing.core.context import Context
  from trueseeing.core.patch import Patcher
  from trueseeing.core.sign import Unsigner, Resigner

class ExploitMode:
  _files: List[str]
  def __init__(self, files: List[str]) -> None:
    self._files = files

  async def invoke(self, mode: str, no_cache_mode: bool = False) -> int:
    for f in self._files:
      try:
        if mode == 'resign':
          await ExploitResign(f, os.path.basename(f).replace('.apk', '-resigned.apk')).exploit()
        elif mode == 'unsign':
          await ExploitUnsign(f, os.path.basename(f).replace('.apk', '-unsigned.apk')).exploit()
        elif mode == 'enable-debug':
          await ExploitEnableDebug(f, os.path.basename(f).replace('.apk', '-debuggable.apk')).exploit()
        elif mode == 'enable-backup':
          await ExploitEnableBackup(f, os.path.basename(f).replace('.apk', '-backupable.apk')).exploit()
        elif mode == 'disable-pinning':
          await ExploitDisablePinning(f, os.path.basename(f).replace('.apk', '-unpinned.apk')).exploit()
      finally:
        if no_cache_mode:
          from trueseeing.core.context import Context
          Context(f, []).remove()

    return 0

class ExploitUnsign:
  _unsigner: Unsigner
  def __init__(self, apk: str, out: str) -> None:
    from trueseeing.core.sign import Unsigner
    self._unsigner = Unsigner(apk, out)

  async def exploit(self) -> None:
    await self._unsigner.unsign()

class ExploitResign:
  _resigner: Resigner
  def __init__(self, apk: str, out: str) -> None:
    from trueseeing.core.sign import Resigner
    self._resigner = Resigner(apk, out)

  async def exploit(self) -> None:
    await self._resigner.resign()

class ExploitEnableDebug:
  _patcher: Patcher
  def __init__(self, apk: str, out: str) -> None:
    from trueseeing.core.patch import Patcher
    self._patcher = Patcher(apk, out)

  async def exploit(self) -> None:
    await self._patcher.apply(self)

  def apply(self, context: Context) -> None:
    manifest = context.parsed_manifest()
    for e in manifest.xpath('.//application'):
      e.attrib['{http://schemas.android.com/apk/res/android}debuggable'] = "true"
    with context.store().db as c:
      c.execute('replace into patches (path, blob) values (:path,:blob)', dict(path='AndroidManifest.xml', blob=context.manifest_as_xml(manifest)))

class ExploitEnableBackup:
  _patcher: Patcher
  def __init__(self, apk: str, out: str) -> None:
    from trueseeing.core.patch import Patcher
    self._patcher = Patcher(apk, out)

  async def exploit(self) -> None:
    await self._patcher.apply(self)

  def apply(self, context: Context) -> None:
    manifest = context.parsed_manifest()
    for e in manifest.xpath('.//application'):
      e.attrib['{http://schemas.android.com/apk/res/android}allowBackup'] = "true"
    with context.store().db as c:
      c.execute('replace into patches (path, blob) values (:path,:blob)', dict(path='AndroidManifest.xml', blob=context.manifest_as_xml(manifest)))

class ExploitDisablePinning:
  _patcher: Patcher
  def __init__(self, apk: str, out: str) -> None:
    from trueseeing.core.patch import Patcher
    self._patcher = Patcher(apk, out)

  async def exploit(self) -> None:
    await self._patcher.apply(self)

  def apply(self, context: Context) -> None:
    import lxml.etree as ET
    manifest = context.parsed_manifest()
    for e in manifest.xpath('.//application'):
      e.attrib['{http://schemas.android.com/apk/res/android}networkSecurityConfig'] = "@xml/network_security_config"

    with context.store().db as c:
      c.execute('replace into patches (path, blob) values (:path,:blob)', dict(path='AndroidManifest.xml', blob=context.manifest_as_xml(manifest)))
      c.execute('replace into patches (path, blob) values (:path,:blob)', dict(path='resources/package_1/res/xml/network_security_config.xml', blob=b'''\
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <base-config cleartextTrafficPermitted="true">
    <trust-anchors>
      <certificates src="system" />
      <certificates src="user" />
    </trust-anchors>
  </base-config>
</network-security-config>
'''))
      for r, in c.execute('select blob from files where path=:path', dict(path='resources/package_1/res/values/public.xml')):
        root = ET.fromstring(r, parser=ET.XMLParser(recover=True))
      if root.xpath('./public[@type="xml"]'):
        maxid = max(int(e.attrib["id"], 16) for e in root.xpath('./public[@type="xml"]'))
        n = ET.SubElement(root, 'public')
        n.attrib['id'] = f'0x{maxid+1:08x}'
        n.attrib['type'] = 'xml'
        n.attrib['name'] = 'network_security_config'
      else:
        maxid = (max(int(e.attrib["id"], 16) for e in root.xpath('./public')) & 0xffff0000)
        n = ET.SubElement(root, 'public')
        n.attrib['id'] = f'0x{maxid+0x10000:08x}'
        n.attrib['type'] = 'xml'
        n.attrib['name'] = 'network_security_config'
      c.execute('replace into patches (path, blob) values (:path,:blob)', dict(path='resources/package_1/res/values/public.xml', blob=ET.tostring(root)))
