diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 365fa0e..c37bc38 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-18.04 strategy: matrix: - python-version: [2.7] + python-version: [2.7, 3.6, 3.7, 3.8] steps: - name: Checkout pysap uses: actions/checkout@v2 @@ -21,9 +21,9 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python2 -m pip install --upgrade pip wheel - python2 -m pip install flake8 six - python2 -m pip install -r requirements.txt + python -m pip install --upgrade pip wheel + python -m pip install flake8 six + python -m pip install -r requirements.txt - name: Run flake8 tests run: | flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics @@ -36,8 +36,8 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-18.04, macos-latest] - python-version: [2.7] + os: [ubuntu-latest, macos-latest] + python-version: [2.7, 3.6, 3.7, 3.8] experimental: [false] include: - os: windows-latest @@ -66,7 +66,7 @@ jobs: - name: Install Python dependencies run: | python -m pip install --upgrade pip wheel - python -m pip install -r requirements.txt + python -m pip install -r requirements.txt -r requirements-tests.txt - name: Run unit tests run: | @@ -86,7 +86,7 @@ jobs: runs-on: ubuntu-18.04 strategy: matrix: - python-version: [2.7] + python-version: [2.7, 3.6, 3.7, 3.8] steps: - name: Checkout pysap uses: actions/checkout@v2 @@ -99,22 +99,23 @@ jobs: sudo apt-get install pandoc texlive-latex-base - name: Install Python dependencies run: | - python2 -m pip install --upgrade pip wheel - python2 -m pip install -r requirements-docs.txt + python -m pip install --upgrade pip wheel + python -m pip install -r requirements-docs.txt + - name: Install the library run: | - python2 setup.py install + python setup.py install - name: Pre-run documentation notebooks run: | - python2 setup.py notebooks + python setup.py notebooks - name: Build source artifact run: | - python2 setup.py sdist + python setup.py sdist - name: Build documentation run: | - python2 setup.py doc + python setup.py doc - name: Upload source artifact uses: actions/upload-artifact@v2 diff --git a/ChangeLog.md b/ChangeLog.md index ddc7918..a41c18e 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,9 +1,10 @@ Changelog ========= -v0.1.20 - 2022-XX-XX --------------------- +v0.2.1 - 2022-XX-XX +------------------- +- Python 2/3 compatible codebase - `pysap/SAPCredv2.py`: Added subject fields instead of commonName for LPS-enabled credentials ([\#35](https://github.com/SecureAuthCorp/pysap/issues/35)). Thanks [@rstenet](https://github.com/rstenet)! - `pysap/SAPCredv2.py`: Add support for cipher format version 1 with 3DES ([\#35](https://github.com/SecureAuthCorp/pysap/issues/35) and [\#37](https://github.com/SecureAuthCorp/pysap/pull/37)). Thanks [@rstenet](https://github.com/rstenet)! - `pysap/SAPHDB.py`: Added missing `StatementContextOption` values (see [\#22](https://github.com/SecureAuthCorp/SAP-Dissection-plug-in-for-Wireshark/issues/22)). diff --git a/README.md b/README.md index af902a9..f64b4e7 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ pysap - Python library for crafting SAP's network protocols packets SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. -Version 0.1.20.dev0 (XXX 2022) +Version 0.2.1.dev0 (XXX 2022) Overview @@ -21,7 +21,7 @@ formats as well. While some of them are standard and well-known protocols, other are proprietaries and public information is generally not available. [pysap](https://www.secureauth.com/labs/open-source-tools/pysap) -is an open source Python 2 library that provides modules for crafting and sending packets +is an open source Python 2/3 library that provides modules for crafting and sending packets using SAP's `NI`, `Diag`, `Enqueue`, `Router`, `MS`, `SNC`, `IGS`, `RFC` and `HDB` protocols. In addition, support for creating and parsing different proprietary file formats is included. The modules are built on top of [Scapy](https://scapy.net/) and are @@ -67,9 +67,7 @@ To install pysap simply run: $ python -m pip install pysap -pysap is compatible and tested with Python 2.7. A Python 2/3 compatible version -is [in the workings](https://github.com/SecureAuthCorp/pysap/tree/python2-3) but -it's [not finalized yet](https://github.com/SecureAuthCorp/pysap/projects/1). +pysap is compatible and tested with Python 2.7/3.8. Documentation ------------- diff --git a/bin/pysapcar b/bin/pysapcar index 94abb1d..3630117 100755 --- a/bin/pysapcar +++ b/bin/pysapcar @@ -17,300 +17,8 @@ # Martin Gallo (@martingalloar) from SecureAuth's Innovation Labs team. # -# Standard imports -import logging -from sys import stdin -from os import makedirs, utime, path -from os import sep as dir_separator -from argparse import ArgumentParser -# Custom imports -import pysap -from pysapcompress import DecompressError -from pysap.SAPCAR import SAPCARArchive, SAPCARInvalidChecksumException, SAPCARInvalidFileException - - -# Try to import OS-dependent functions -try: - from os import chmod, fchmod -except ImportError: - chmod = fchmod = None - - -pysapcar_usage = """ - -create archive with specified files: -pysapcar -c[v][f archive] [file1 file2 [/n filename] ...] - -list the contents of an archive: -pysapcar -t[v][f archive] [file1 file2 ...] - -extract files from an archive: -pysapcar -x[v][f archive] [-o outdir] [file1 file2 ...] - -append files to an archive: -pysapcar -a[v][f archive] [file1 file2 [/n filename] ...] - -""" - - -class PySAPCAR(object): - - # Private attributes - _logger = None - - # Instance attributes - mode = None - log_level = None - archive_fd = None - - @staticmethod - def parse_options(): - """Parses command-line options. - """ - description = "Basic and experimental implementation of SAPCAR archive format." - - parser = ArgumentParser(usage=pysapcar_usage, description=description, epilog=pysap.epilog) - parser.add_argument("--version", action="version", version="%(prog)s {}".format(pysap.__version__)) - - # Commands - parser.add_argument("-c", dest="create", action="store_true", help="Create archive with specified files") - parser.add_argument("-x", dest="extract", action="store_true", help="Extract files from an archive") - parser.add_argument("-t", dest="list", action="store_true", help="List the contents of an archive") - parser.add_argument("-a", dest="append", action="store_true", help="Append files to an archive") - parser.add_argument("-f", dest="filename", help="Archive filename", metavar="FILE") - parser.add_argument("-o", dest="outdir", help="Path to directory where to extract files") - - misc = parser.add_argument_group("Misc options") - misc.add_argument("-v", "--verbose", dest="verbose", action="store_true", help="Verbose output") - misc.add_argument("-e", "--enforce-checksum", dest="enforce_checksum", action="store_true", - help="Whether the checksum validation is enforced. A file with an invalid checksum won't be " - "extracted. When not set, only a warning would be thrown if checksum is invalid.") - misc.add_argument("-b", "--break-on-error", dest="break_on_error", action="store_true", - help="Whether the extraction would continue if an error is identified.") - - (options, args) = parser.parse_known_args() - - return options, args - - @property - def logger(self): - """Sets the logger of the cli tool. - """ - if self._logger is None: - self._logger = logging.getLogger("pysap.pysapcar") - self._logger.setLevel(logging.DEBUG if self.verbose else logging.INFO) - self._logger.addHandler(logging.StreamHandler()) - return self._logger - - def main(self): - """Main routine for parsing options and dispatch desired action. - """ - options, args = self.parse_options() - - # Set the verbosity - self.verbose = options.verbose - - self.logger.info("pysapcar version: %s", pysap.__version__) - - # Check the mode the archive file should be opened - if options.list or options.extract: - self.mode = "r" - elif options.create: - self.mode = "w" - elif options.append: - self.mode = "r+" - else: # default to read mode - self.mode = "r" - - # Opens the input/output file - self.archive_fd = None - if options.filename: - try: - self.archive_fd = open(options.filename, self.mode) - except IOError as e: - self.logger.error("pysapcar: error opening '%s' (%s)" % (options.filename, e.strerror)) - return - else: - self.archive_fd = stdin - - # Execute the action - try: - if options.create or options.append: - self.append(options, args) - elif options.list: - self.list(options, args) - elif options.extract: - self.extract(options, args) - finally: - self.archive_fd.close() - - def open_archive(self): - """Opens the archive file to work with it and returns - the SAP Car Archive object. - """ - try: - sapcar = SAPCARArchive(self.archive_fd, mode=self.mode) - self.logger.info("pysapcar: Processing archive '%s' (version %s)", self.archive_fd.name, sapcar.version) - except Exception as e: - self.logger.error("pysapcar: Error processing archive '%s' (%s)", self.archive_fd.name, e.message) - return None - return sapcar - - @staticmethod - def target_files(filenames, target_filenames=None): - """Generates the list of files to work on. It calculates - the intersection between the file names selected in - command-line and the ones in the archive to work on. - - :param filenames: filenames in the archive - :param target_filenames: filenames to work on - - :return: filename - """ - files = set(filenames) - if target_filenames: - files = files.intersection(set(target_filenames)) - - for filename in files: - yield filename - - def append(self, options, args): - """Appends a file to the archive file. - """ - - if len(args) < 1: - self.logger.error("pysapcar: no files specified for appending") - return - - # Open the archive file - sapcar = self.open_archive() - if not sapcar: - return - - while len(args): - filename = filename_in_archive = args.pop(0) - - self.logger.debug(args) - if len(args) >= 2 and args[0] == "/n": - args.pop(0) - filename_in_archive = args.pop(0) - - sapcar.add_file(filename, archive_filename=filename_in_archive) - if filename != filename_in_archive: - self.logger.info("d %s (original name %s)", filename_in_archive, filename) - else: - self.logger.info("d %s", filename) - - sapcar.write() - - def list(self, options, args): - """List files inside the archive file and print their - attributes: permissions, size, timestamp and filename. - """ - # Open the archive file - sapcar = self.open_archive() - if not sapcar: - return - # Print the info of each file - for filename in self.target_files(sapcar.files_names, args): - fil = sapcar.files[filename] - self.logger.info("{} {:>10} {} {}".format(fil.permissions, fil.size, fil.timestamp, fil.filename)) - - def extract(self, options, args): - """Extract files from the archive file. - """ - CONTINUE = 1 - SKIP = 2 - STOP = 3 - - # Open the archive file - sapcar = self.open_archive() - if not sapcar: - return - - # Warn if permissions can't be set - if not chmod: - self.logger.warning("pysapcar: Setting extracted files permissions not implemented in this platform") - - # Extract each file in the archive - no = 0 - for filename in self.target_files(sapcar.files_names, args): - flag = CONTINUE - fil = sapcar.files[filename] - filename = path.normpath(filename.replace("\x00", "")) # Take out null bytes if found - if options.outdir: - # Have to strip directory separator from the beginning of the file name, because path.join disregards - # all previous components if any of the following components is an absolute path - filename = path.join(path.normpath(options.outdir), filename.lstrip(dir_separator)) - - if fil.is_directory(): - # If the directory doesn't exist, create it and set permissions and timestamp - if not path.exists(filename): - makedirs(filename) - if chmod: - chmod(filename, fil.perm_mode) - utime(filename, (fil.timestamp_raw, fil.timestamp_raw)) - self.logger.info("d %s", filename) - no += 1 - - elif fil.is_file(): - # If the file references a directory that is not there, create it first - file_dirname = path.dirname(filename) - if file_dirname and not path.exists(file_dirname): - # mkdir barfs if archive contains /foo/bar/bash but not /foo/bar directory. - # makedirs creates intermediate directories as well - makedirs(file_dirname) - self.logger.info("d %s", file_dirname) - - # Try to extract the file and handle potential errors - try: - data = fil.open(enforce_checksum=options.enforce_checksum).read() - except (SAPCARInvalidFileException, DecompressError) as e: - self.logger.error("pysapcar: Invalid SAP CAR file '%s' (%s)", self.archive_fd.name, e.message) - if options.break_on_error: - flag = STOP - else: - flag = SKIP - except SAPCARInvalidChecksumException: - self.logger.error("pysapcar: Invalid checksum found for file '%s'", fil.filename) - if options.enforce_checksum: - flag = STOP - - # Check the result before starting to write the file - if flag == SKIP: - self.logger.info("pysapcar: Skipping execution of file '%s'", fil.filename) - continue - elif flag == STOP: - self.logger.info("pysapcar: Stopping extraction") - break - - # Write the new file and set permissions - try: - with open(filename, "wb") as new_file: - new_file.write(data) - if fchmod: - fchmod(new_file.fileno(), fil.perm_mode) - except IOError as e: - self.logger.error("pysapcar: Failed to extract file '%s', reason: %s", filename, e.strerror) - if options.break_on_error: - break - - # Set the timestamp - utime(filename, (fil.timestamp_raw, fil.timestamp_raw)) - - # If this path is reached and checksum is not valid, means checksum is not enforced, so we should warn - # only. - if not fil.check_checksum(): - self.logger.warning("pysapcar: checksum error in '%s' !", filename) - - self.logger.info("d %s", filename) - no += 1 - else: - self.logger.warning("pysapcar: Invalid file type '%s'", filename) - - self.logger.info("pysapcar: %d file(s) processed", no) +from pysap.sapcarcli import PySAPCAR if __name__ == "__main__": - pysapcar = PySAPCAR() - pysapcar.main() + PySAPCAR().main() diff --git a/bin/pysapgenpse b/bin/pysapgenpse index 19c8ddf..76b4af5 100755 --- a/bin/pysapgenpse +++ b/bin/pysapgenpse @@ -140,11 +140,11 @@ class PySAPGenPSE(object): """ try: - with open(filename) as f: + with open(filename, 'rb') as f: obj = cls(f.read()) self.logger.info("pysapgenpse: Reading {} file '{}'".format(type, filename)) except IOError as e: - self.logger.error("pysapgenpse: Error reading {} file '{}' ({})".format(type, filename, e.message)) + self.logger.error("pysapgenpse: Error reading {} file '{}' ({})".format(type, filename, str(e))) return None return obj @@ -230,7 +230,7 @@ class PySAPGenPSE(object): self.decrypt_cred(cred, options.username, options.output, options.decrypt_provider) except Exception as e: self.logger.error("pysapgenpse: Error trying to decrypt with username '{}'".format(options.username)) - self.logger.error(e) + self.logger.error(str(e)) i += 1 @@ -259,7 +259,7 @@ class PySAPGenPSE(object): pin = plain.decrypt_provider(cred) except Exception as e: self.logger.error("pysapgenpse: Unable to decrypt using the provider {} ({}), writing plain blob".format( - plain.option1.val, e.message)) + plain.option1.val, str(e))) pin = plain.pin.val else: pin = plain.pin.val diff --git a/examples/diag_rogue_server.py b/examples/diag_rogue_server.py index 67965ad..7669228 100755 --- a/examples/diag_rogue_server.py +++ b/examples/diag_rogue_server.py @@ -83,35 +83,35 @@ def make_login_screen(self): SAPDiagItem(item_value='SAPMSYST ', item_type=16, item_id=6, item_sid=13), SAPDiagItem(item_value='0020 ', item_type=16, item_id=6, item_sid=16), SAPDiagItem(item_value='0020', item_type=16, item_id=6, item_sid=14), - SAPDiagItem(item_value=SAPDiagMenuEntries(entries=[SAPDiagMenuEntry(accelerator='D', text=self.session_title, position_1=1, flag_TERM_VKEY=1L, return_code_1=1, flag_TERM_SEL=1L, length=24 + len(self.session_title)), + SAPDiagItem(item_value=SAPDiagMenuEntries(entries=[SAPDiagMenuEntry(accelerator='D', text=self.session_title, position_1=1, flag_TERM_VKEY=1, return_code_1=1, flag_TERM_SEL=1, length=24 + len(self.session_title)), ]), item_type=18, item_id=11, item_sid=1), - SAPDiagItem(item_value=SAPDiagMenuEntries(entries=[SAPDiagMenuEntry(accelerator='', text='New password', virtual_key=5, return_code_1=5, info='New password', flag_TERM_SEL=1L, length=47), + SAPDiagItem(item_value=SAPDiagMenuEntries(entries=[SAPDiagMenuEntry(accelerator='', text='New password', virtual_key=5, return_code_1=5, info='New password', flag_TERM_SEL=1, length=47), ]), item_type=18, item_id=11, item_sid=3), - SAPDiagItem(item_value=SAPDiagMenuEntries(entries=[SAPDiagMenuEntry(accelerator='', text='New password', virtual_key=5, position_1=1, flag_TERM_SEL=1L, length=35), - SAPDiagMenuEntry(accelerator='', text='Log off', virtual_key=15, position_1=2, return_code_1=1, flag_TERM_SEL=1L, length=30) + SAPDiagItem(item_value=SAPDiagMenuEntries(entries=[SAPDiagMenuEntry(accelerator='', text='New password', virtual_key=5, position_1=1, flag_TERM_SEL=1, length=35), + SAPDiagMenuEntry(accelerator='', text='Log off', virtual_key=15, position_1=2, return_code_1=1, flag_TERM_SEL=1, length=30) ]), item_type=18, item_id=11, item_sid=4), SAPDiagItem(item_value=self.session_title, item_type=16, item_id=12, item_sid=9), - SAPDiagItem(item_value=SAPDiagDyntAtom(items=[SAPDiagDyntAtomItem(field2_text='Client ', field2_maxnrchars=18, dlg_flag_2=2, atom_length=37, etype=132, attr_DIAG_BSD_PROTECTED=1L, field2_mlen=18, field2_dlen=18, attr_DIAG_BSD_PROPFONT=1L, block=1, col=1), - SAPDiagDyntAtomItem(dlg_flag_2=2, atom_length=24, name_text='RSYST-MANDT', etype=114, attr_DIAG_BSD_PROTECTED=1L, attr_DIAG_BSD_PROPFONT=1L, block=1, col=1), - SAPDiagDyntAtomItem(field2_text=self.client, attr_DIAG_BSD_YES3D=1L, field2_maxnrchars=3, atom_length=22, etype=130, field2_mlen=3, field2_dlen=3, block=1, col=20), - SAPDiagDyntAtomItem(attr_DIAG_BSD_YES3D=1L, atom_length=24, name_text='RSYST-MANDT', etype=114, block=1, col=20), - SAPDiagDyntAtomItem(attr_DIAG_BSD_YES3D=1L, atom_length=79, xmlprop_text='Client', etype=120, block=1, col=20), - SAPDiagDyntAtomItem(field2_text='User ', field2_maxnrchars=18, row=2, dlg_flag_2=3, atom_length=37, etype=132, attr_DIAG_BSD_PROTECTED=1L, field2_mlen=18, field2_dlen=18, attr_DIAG_BSD_PROPFONT=1L, block=1, col=1), - SAPDiagDyntAtomItem(row=2, dlg_flag_2=3, atom_length=24, name_text='RSYST-BNAME', etype=114, attr_DIAG_BSD_PROTECTED=1L, attr_DIAG_BSD_PROPFONT=1L, block=1, col=1), - SAPDiagDyntAtomItem(field2_text='? ', attr_DIAG_BSD_YES3D=1L, field2_maxnrchars=12, row=2, dlg_flag_2=1, atom_length=31, etype=130, field2_mlen=12, field2_dlen=12, block=1, col=20), - SAPDiagDyntAtomItem(attr_DIAG_BSD_YES3D=1L, row=2, dlg_flag_2=1, atom_length=24, name_text='RSYST-BNAME', etype=114, block=1, col=20), - SAPDiagDyntAtomItem(attr_DIAG_BSD_YES3D=1L, row=2, dlg_flag_2=1, atom_length=82, xmlprop_text='User name', etype=120, block=1, col=20), - SAPDiagDyntAtomItem(field2_text='@\\QUp to 40 Chars (Case-Sens.)@Password ', field2_maxnrchars=52, row=3, dlg_flag_2=3, dlg_flag_1=4, atom_length=71, etype=132, attr_DIAG_BSD_PROTECTED=1L, field2_mlen=18, field2_dlen=52, attr_DIAG_BSD_PROPFONT=1L, block=1, col=1), - SAPDiagDyntAtomItem(row=3, dlg_flag_2=3, dlg_flag_1=4, atom_length=24, name_text='RSYST-BCODE', etype=114, attr_DIAG_BSD_PROTECTED=1L, attr_DIAG_BSD_PROPFONT=1L, block=1, col=1), - SAPDiagDyntAtomItem(field2_text='? ', attr_DIAG_BSD_YES3D=1L, field2_maxnrchars=40, row=3, dlg_flag_2=1, dlg_flag_1=4, atom_length=59, etype=130, attr_DIAG_BSD_INVISIBLE=1L, field2_mlen=12, field2_dlen=40, block=1, col=20), - SAPDiagDyntAtomItem(attr_DIAG_BSD_YES3D=1L, row=3, dlg_flag_2=1, dlg_flag_1=4, atom_length=24, name_text='RSYST-BCODE', etype=114, attr_DIAG_BSD_INVISIBLE=1L, block=1, col=20), - SAPDiagDyntAtomItem(attr_DIAG_BSD_YES3D=1L, row=3, dlg_flag_2=1, dlg_flag_1=4, atom_length=86, xmlprop_text='User password', etype=120, attr_DIAG_BSD_INVISIBLE=1L, block=1, col=20), - SAPDiagDyntAtomItem(field2_text='Language ', field2_maxnrchars=18, row=5, dlg_flag_2=2, atom_length=37, etype=132, attr_DIAG_BSD_PROTECTED=1L, field2_mlen=18, field2_dlen=18, attr_DIAG_BSD_PROPFONT=1L, block=1, col=1), - SAPDiagDyntAtomItem(row=5, dlg_flag_2=2, atom_length=24, name_text='RSYST-LANGU', etype=114, attr_DIAG_BSD_PROTECTED=1L, attr_DIAG_BSD_PROPFONT=1L, block=1, col=1), - SAPDiagDyntAtomItem(field2_text=' ', attr_DIAG_BSD_YES3D=1L, field2_maxnrchars=2, row=5, atom_length=21, etype=130, field2_mlen=2, field2_dlen=2, block=1, col=20), - SAPDiagDyntAtomItem(attr_DIAG_BSD_YES3D=1L, row=5, atom_length=24, name_text='RSYST-LANGU', etype=114, block=1, col=20), - SAPDiagDyntAtomItem(atom_length=81, attr_DIAG_BSD_YES3D=1L, xmlprop_text='Language', etype=120, col=20, block=1, row=5), - SAPDiagDyntAtomItem(field2_text=self.session_title, field2_maxnrchars=18, row=7, dlg_flag_2=2, atom_length=37, etype=132, attr_DIAG_BSD_PROTECTED=1L, field2_mlen=18, field2_dlen=18, attr_DIAG_BSD_PROPFONT=1L, block=1, col=1), + SAPDiagItem(item_value=SAPDiagDyntAtom(items=[SAPDiagDyntAtomItem(field2_text='Client ', field2_maxnrchars=18, dlg_flag_2=2, atom_length=37, etype=132, attr_DIAG_BSD_PROTECTED=1, field2_mlen=18, field2_dlen=18, attr_DIAG_BSD_PROPFONT=1, block=1, col=1), + SAPDiagDyntAtomItem(dlg_flag_2=2, atom_length=24, name_text='RSYST-MANDT', etype=114, attr_DIAG_BSD_PROTECTED=1, attr_DIAG_BSD_PROPFONT=1, block=1, col=1), + SAPDiagDyntAtomItem(field2_text=self.client, attr_DIAG_BSD_YES3D=1, field2_maxnrchars=3, atom_length=22, etype=130, field2_mlen=3, field2_dlen=3, block=1, col=20), + SAPDiagDyntAtomItem(attr_DIAG_BSD_YES3D=1, atom_length=24, name_text='RSYST-MANDT', etype=114, block=1, col=20), + SAPDiagDyntAtomItem(attr_DIAG_BSD_YES3D=1, atom_length=79, xmlprop_text='Client', etype=120, block=1, col=20), + SAPDiagDyntAtomItem(field2_text='User ', field2_maxnrchars=18, row=2, dlg_flag_2=3, atom_length=37, etype=132, attr_DIAG_BSD_PROTECTED=1, field2_mlen=18, field2_dlen=18, attr_DIAG_BSD_PROPFONT=1, block=1, col=1), + SAPDiagDyntAtomItem(row=2, dlg_flag_2=3, atom_length=24, name_text='RSYST-BNAME', etype=114, attr_DIAG_BSD_PROTECTED=1, attr_DIAG_BSD_PROPFONT=1, block=1, col=1), + SAPDiagDyntAtomItem(field2_text='? ', attr_DIAG_BSD_YES3D=1, field2_maxnrchars=12, row=2, dlg_flag_2=1, atom_length=31, etype=130, field2_mlen=12, field2_dlen=12, block=1, col=20), + SAPDiagDyntAtomItem(attr_DIAG_BSD_YES3D=1, row=2, dlg_flag_2=1, atom_length=24, name_text='RSYST-BNAME', etype=114, block=1, col=20), + SAPDiagDyntAtomItem(attr_DIAG_BSD_YES3D=1, row=2, dlg_flag_2=1, atom_length=82, xmlprop_text='User name', etype=120, block=1, col=20), + SAPDiagDyntAtomItem(field2_text='@\\QUp to 40 Chars (Case-Sens.)@Password ', field2_maxnrchars=52, row=3, dlg_flag_2=3, dlg_flag_1=4, atom_length=71, etype=132, attr_DIAG_BSD_PROTECTED=1, field2_mlen=18, field2_dlen=52, attr_DIAG_BSD_PROPFONT=1, block=1, col=1), + SAPDiagDyntAtomItem(row=3, dlg_flag_2=3, dlg_flag_1=4, atom_length=24, name_text='RSYST-BCODE', etype=114, attr_DIAG_BSD_PROTECTED=1, attr_DIAG_BSD_PROPFONT=1, block=1, col=1), + SAPDiagDyntAtomItem(field2_text='? ', attr_DIAG_BSD_YES3D=1, field2_maxnrchars=40, row=3, dlg_flag_2=1, dlg_flag_1=4, atom_length=59, etype=130, attr_DIAG_BSD_INVISIBLE=1, field2_mlen=12, field2_dlen=40, block=1, col=20), + SAPDiagDyntAtomItem(attr_DIAG_BSD_YES3D=1, row=3, dlg_flag_2=1, dlg_flag_1=4, atom_length=24, name_text='RSYST-BCODE', etype=114, attr_DIAG_BSD_INVISIBLE=1, block=1, col=20), + SAPDiagDyntAtomItem(attr_DIAG_BSD_YES3D=1, row=3, dlg_flag_2=1, dlg_flag_1=4, atom_length=86, xmlprop_text='User password', etype=120, attr_DIAG_BSD_INVISIBLE=1, block=1, col=20), + SAPDiagDyntAtomItem(field2_text='Language ', field2_maxnrchars=18, row=5, dlg_flag_2=2, atom_length=37, etype=132, attr_DIAG_BSD_PROTECTED=1, field2_mlen=18, field2_dlen=18, attr_DIAG_BSD_PROPFONT=1, block=1, col=1), + SAPDiagDyntAtomItem(row=5, dlg_flag_2=2, atom_length=24, name_text='RSYST-LANGU', etype=114, attr_DIAG_BSD_PROTECTED=1, attr_DIAG_BSD_PROPFONT=1, block=1, col=1), + SAPDiagDyntAtomItem(field2_text=' ', attr_DIAG_BSD_YES3D=1, field2_maxnrchars=2, row=5, atom_length=21, etype=130, field2_mlen=2, field2_dlen=2, block=1, col=20), + SAPDiagDyntAtomItem(attr_DIAG_BSD_YES3D=1, row=5, atom_length=24, name_text='RSYST-LANGU', etype=114, block=1, col=20), + SAPDiagDyntAtomItem(atom_length=81, attr_DIAG_BSD_YES3D=1, xmlprop_text='Language', etype=120, col=20, block=1, row=5), + SAPDiagDyntAtomItem(field2_text=self.session_title, field2_maxnrchars=18, row=7, dlg_flag_2=2, atom_length=37, etype=132, attr_DIAG_BSD_PROTECTED=1, field2_mlen=18, field2_dlen=18, attr_DIAG_BSD_PROPFONT=1, block=1, col=1), ]), item_type=18, item_id=9, item_sid=2), ] diff --git a/examples/dlmanager_decrypt.py b/examples/dlmanager_decrypt.py index a10cff9..48716fe 100755 --- a/examples/dlmanager_decrypt.py +++ b/examples/dlmanager_decrypt.py @@ -177,7 +177,7 @@ def parse_config_file(filename, decrypt=False, serial_number=None): print("[*] Read %d config values from config file" % len(data)) for item in data: value = "" - if isinstance(data[item], basestring): + if isinstance(data[item], str): value = data[item] elif "value" in data[item]: value = data[item]["value"] diff --git a/examples/hdb_auth.py b/examples/hdb_auth.py index 699fe97..cf7f90a 100755 --- a/examples/hdb_auth.py +++ b/examples/hdb_auth.py @@ -203,9 +203,9 @@ def main(): logging.debug("[*] Connection with HANA database server closed") except SAPHDBAuthenticationError as e: - logging.error("[-] Authentication error: %s" % e.message) + logging.error("[-] Authentication error: %s" % str(e)) except SAPHDBConnectionError as e: - logging.error("[-] Connection error: %s" % e.message) + logging.error("[-] Connection error: %s" % str(e)) except KeyboardInterrupt: logging.info("[-] Connection canceled") diff --git a/examples/hdb_discovery.py b/examples/hdb_discovery.py index b466c37..412d752 100755 --- a/examples/hdb_discovery.py +++ b/examples/hdb_discovery.py @@ -24,6 +24,7 @@ from argparse import ArgumentParser from socket import error as SocketError # External imports +from six import iteritems from scapy.config import conf # Custom imports import pysap @@ -144,7 +145,7 @@ def main(): # List the values in the returned DBConnectInfo part hdb_dbconnectinfo_response_part = hdb_dbconnectinfo_response.segments[0].parts[0] - for key, name in SAPHDBPartDBConnectInfo.option_keys.items(): + for key, name in iteritems(SAPHDBPartDBConnectInfo.option_keys): value = hdb_get_part_kind_option(hdb_dbconnectinfo_response_part, key) if value is not None: results[tenant][name] = value @@ -162,7 +163,7 @@ def main(): except SocketError: logging.error("[-] Tenant '%s' doesn't exist" % tenant) except SAPHDBConnectionError as e: - logging.error("[-] Connection error: %s" % e.message) + logging.error("[-] Connection error: %s" % str(e)) except KeyboardInterrupt: logging.info("[-] Connection canceled") diff --git a/pysap/SAPCAR.py b/pysap/SAPCAR.py index 4155af3..90906d3 100644 --- a/pysap/SAPCAR.py +++ b/pysap/SAPCAR.py @@ -17,68 +17,72 @@ # # Standard imports +from __future__ import unicode_literals +import six import stat from zlib import crc32 from struct import pack from datetime import datetime from os import stat as os_stat -from cStringIO import StringIO +from io import BytesIO # External imports from scapy.packet import Packet from scapy.fields import (ByteField, ByteEnumField, LEIntField, FieldLenField, PacketField, StrFixedLenField, PacketListField, ConditionalField, LESignedIntField, StrField, LELongField) # Custom imports +from pysap.utils import unicode from pysap.utils.fields import (PacketNoPadded, StrNullFixedLenField, PacketListStopField) -from pysapcompress import (decompress, compress, ALG_LZH, CompressError, - DecompressError) - - -# Filemode code obtained from Python 3 stat.py -_filemode_table = ( - ((stat.S_IFLNK, "l"), - (stat.S_IFREG, "-"), - (stat.S_IFBLK, "b"), - (stat.S_IFDIR, "d"), - (stat.S_IFCHR, "c"), - (stat.S_IFIFO, "p")), - - ((stat.S_IRUSR, "r"),), - ((stat.S_IWUSR, "w"),), - ((stat.S_IXUSR | stat.S_ISUID, "s"), - (stat.S_ISUID, "S"), - (stat.S_IXUSR, "x")), - - ((stat.S_IRGRP, "r"),), - ((stat.S_IWGRP, "w"),), - ((stat.S_IXGRP | stat.S_ISGID, "s"), - (stat.S_ISGID, "S"), - (stat.S_IXGRP, "x")), - - ((stat.S_IROTH, "r"),), - ((stat.S_IWOTH, "w"),), - ((stat.S_IXOTH | stat.S_ISVTX, "t"), - (stat.S_ISVTX, "T"), - (stat.S_IXOTH, "x")) -) +from pysapcompress import (decompress, compress, ALG_LZH, CompressError, DecompressError) + + +if six.PY2: + # Filemode code obtained from Python 3 stat.py + _filemode_table = ( + ((stat.S_IFLNK, "l"), + (stat.S_IFREG, "-"), + (stat.S_IFBLK, "b"), + (stat.S_IFDIR, "d"), + (stat.S_IFCHR, "c"), + (stat.S_IFIFO, "p")), + + ((stat.S_IRUSR, "r"),), + ((stat.S_IWUSR, "w"),), + ((stat.S_IXUSR | stat.S_ISUID, "s"), + (stat.S_ISUID, "S"), + (stat.S_IXUSR, "x")), + + ((stat.S_IRGRP, "r"),), + ((stat.S_IWGRP, "w"),), + ((stat.S_IXGRP | stat.S_ISGID, "s"), + (stat.S_ISGID, "S"), + (stat.S_IXGRP, "x")), + + ((stat.S_IROTH, "r"),), + ((stat.S_IWOTH, "w"),), + ((stat.S_IXOTH | stat.S_ISVTX, "t"), + (stat.S_ISVTX, "T"), + (stat.S_IXOTH, "x")) + ) + + def filemode(mode): + """Convert a file's mode to a string of the form '-rwxrwxrwx'.""" + perm = [] + for table in _filemode_table: + for bit, char in table: + if mode & bit == bit: + perm.append(char) + break + else: + perm.append("-") + return "".join(perm) +else: + from stat import filemode SIZE_FOUR_GB = 0xffffffff + 1 -def filemode(mode): - """Convert a file's mode to a string of the form '-rwxrwxrwx'.""" - perm = [] - for table in _filemode_table: - for bit, char in table: - if mode & bit == bit: - perm.append(char) - break - else: - perm.append("-") - return "".join(perm) - - class SAPCARInvalidFileException(Exception): """Exception to denote an invalid SAP CAR file""" @@ -97,8 +101,8 @@ class SAPCARCompressedBlobFormat(PacketNoPadded): fields_desc = [ LEIntField("compressed_length", None), LEIntField("uncompress_length", None), - ByteEnumField("algorithm", 0x12, {0x12: "LZH", 0x10: "LZC"}), - StrFixedLenField("magic_bytes", "\x1f\x9d", 2), + ByteEnumField("algorithm", 0x12, {0x12: b"LZH", 0x10: b"LZC"}), + StrFixedLenField("magic_bytes", b"\x1f\x9d", 2), ByteField("special", 2), ConditionalField(StrField("blob", None, remain=4), lambda x: x.compressed_length <= 8), ConditionalField(StrFixedLenField("blob", None, length_from=lambda x: x.compressed_length - 8), @@ -106,17 +110,17 @@ class SAPCARCompressedBlobFormat(PacketNoPadded): ] -SAPCAR_BLOCK_TYPE_COMPRESSED_LAST = "ED" -"""SAP CAR compressed end of data block""" +# SAP CAR compressed end of data block +SAPCAR_BLOCK_TYPE_COMPRESSED_LAST = b"ED" -SAPCAR_BLOCK_TYPE_COMPRESSED = "DA" -"""SAP CAR compressed block""" +# SAP CAR compressed block +SAPCAR_BLOCK_TYPE_COMPRESSED = b"DA" -SAPCAR_BLOCK_TYPE_UNCOMPRESSED_LAST = "UE" -"""SAP CAR uncompressed end of data block""" +# SAP CAR uncompressed end of data block +SAPCAR_BLOCK_TYPE_UNCOMPRESSED_LAST = b"UE" -SAPCAR_BLOCK_TYPE_UNCOMPRESSED = "UD" -"""SAP CAR uncompressed block""" +# SAP CAR uncompressed block +SAPCAR_BLOCK_TYPE_UNCOMPRESSED = b"UD" class SAPCARCompressedBlockFormat(PacketNoPadded): @@ -147,30 +151,31 @@ def sapcar_is_last_block(packet): return packet.type in [SAPCAR_BLOCK_TYPE_COMPRESSED_LAST, SAPCAR_BLOCK_TYPE_UNCOMPRESSED_LAST] -SAPCAR_TYPE_FILE = "RG" -"""SAP CAR regular file string""" +# SAP CAR regular file string +SAPCAR_TYPE_FILE = b"RG" -SAPCAR_TYPE_DIR = "DR" -"""SAP CAR directory string""" +# SAP CAR directory string +SAPCAR_TYPE_DIR = b"DR" -SAPCAR_TYPE_SHORTCUT = "SC" -"""SAP CAR Windows short cut string""" +# SAP CAR Windows short cut string +SAPCAR_TYPE_SHORTCUT = b"SC" -SAPCAR_TYPE_LINK = "LK" -"""SAP CAR Unix soft link string""" +# SAP CAR Unix soft link string +SAPCAR_TYPE_LINK = b"LK" -SAPCAR_TYPE_AS400 = "SV" -"""SAP CAR AS400 save file string""" +# SAP CAR AS400 save file string +SAPCAR_TYPE_AS400 = b"SV" SAPCAR_TYPE_SIGNATURE = "SM" """SAP CAR SIGNATURE.SMF file string""" # XXX: Unsure if this file has any particular treatment in latest versions of SAPCAR +# Version strings are unicode instead of byte strings in order to avoid constant .decode() and .encode() calls +# SAP CAR file format version 2.00 string SAPCAR_VERSION_200 = "2.00" -"""SAP CAR file format version 2.00 string""" +# SAP CAR file format version 2.01 string SAPCAR_VERSION_201 = "2.01" -"""SAP CAR file format version 2.01 string""" class SAPCARArchiveFilev200Format(PacketNoPadded): @@ -234,7 +239,7 @@ def extract(self, fd): if self.file_length == 0: return 0 - compressed = "" + compressed = b"" checksum = 0 exp_length = None @@ -247,7 +252,7 @@ def extract(self, fd): # Store compressed block types for later decompression elif block.type in [SAPCAR_BLOCK_TYPE_COMPRESSED, SAPCAR_BLOCK_TYPE_COMPRESSED_LAST]: # Add compressed block to a buffer, skipping the first 4 bytes of each block (uncompressed length) - compressed += str(block.compressed)[4:] + compressed += six.binary_type(block.compressed)[4:] # If the expected length wasn't already set, do it if not exp_length: exp_length = block.compressed.uncompress_length @@ -259,7 +264,7 @@ def extract(self, fd): checksum = block.checksum # If there was at least one compressed block that set the expected length, decompress it if exp_length: - (_, block_length, block_buffer) = decompress(str(compressed), exp_length) + (_, block_length, block_buffer) = decompress(six.binary_type(compressed), exp_length) if block_length != exp_length or not block_buffer: raise DecompressError("Error decompressing block") fd.write(block_buffer) @@ -279,18 +284,17 @@ class SAPCARArchiveFilev201Format(SAPCARArchiveFilev200Format): is_filename_null_terminated = True -SAPCAR_HEADER_MAGIC_STRING_STANDARD = "CAR\x20" -"""SAP CAR archive header magic string standard""" - -SAPCAR_HEADER_MAGIC_STRING_BACKUP = "CAR\x00" -"""SAP CAR archive header magic string backup file""" +# SAP CAR archive header magic string standard +SAPCAR_HEADER_MAGIC_STRING_STANDARD = b"CAR\x20" +# SAP CAR archive header magic string backup file +SAPCAR_HEADER_MAGIC_STRING_BACKUP = b"CAR\x00" +# SAP CAR file format versions sapcar_archive_file_versions = { SAPCAR_VERSION_200: SAPCARArchiveFilev200Format, SAPCAR_VERSION_201: SAPCARArchiveFilev201Format, } -"""SAP CAR file format versions""" class SAPCARArchiveFormat(Packet): @@ -303,10 +307,11 @@ class SAPCARArchiveFormat(Packet): fields_desc = [ StrFixedLenField("magic_string", SAPCAR_HEADER_MAGIC_STRING_STANDARD, 4), StrFixedLenField("version", SAPCAR_VERSION_201, 4), + # Scapy automatically encodes unicode strings, so need to decode packet version in lambda ConditionalField(PacketListField("files0", None, SAPCARArchiveFilev200Format), - lambda x: x.version == SAPCAR_VERSION_200), + lambda x: x.version.decode() == SAPCAR_VERSION_200), ConditionalField(PacketListField("files1", None, SAPCARArchiveFilev201Format), - lambda x: x.version == SAPCAR_VERSION_201), + lambda x: x.version.decode() == SAPCAR_VERSION_201), ] @@ -348,34 +353,34 @@ def version(self): """The version of the file. :return: version of the file - :rtype: string + :rtype: six.text_type """ - return self._file_format.version + return unicode(self._file_format.version) @property def type(self): """The type of the file. :return: type of the file - :rtype: string + :rtype: six.text_type """ - return self._file_format.type + return unicode(self._file_format.type) @property def filename(self): """The name of the file. :return: name of the file - :rtype: string + :rtype: six.text_type """ - return self._file_format.filename + return unicode(self._file_format.filename) @filename.setter def filename(self, filename): """Sets the name of the file. :param filename: the name of the file - :type filename: string + :type filename: six.text_type """ self._file_format.filename = filename self._file_format.filename_length = len(filename) @@ -405,9 +410,9 @@ def permissions(self): """The permissions of the file. :return: permissions in human-readable format - :rtype: string + :rtype: six.text_type """ - return filemode(self._file_format.perm_mode) + return unicode(filemode(self._file_format.perm_mode)) @permissions.setter def permissions(self, perm_mode): @@ -432,9 +437,9 @@ def timestamp(self): """The timestamp of the file. :return: timestamp in human-readable format - :rtype: string + :rtype: six.text_type """ - return datetime.utcfromtimestamp(self._file_format.timestamp).strftime('%d %b %Y %H:%M') + return unicode(datetime.utcfromtimestamp(self._file_format.timestamp).strftime('%d %b %Y %H:%M')) @timestamp.setter def timestamp(self, timestamp): @@ -496,7 +501,7 @@ def calculate_checksum(data): """Calculates the CRC32 checksum of a given data string. :param data: data to calculate the checksum over - :type data: str + :type data: six.binary_type :return: the CRC32 checksum :rtype: int @@ -509,19 +514,19 @@ def from_file(cls, filename, version=SAPCAR_VERSION_201, archive_filename=None): local file system. :param filename: filename to build the file format object from - :type filename: string + :type filename: six.text_type :param version: version of the file to construct - :type version: string + :type version: six.text_type :param archive_filename: filename to use inside the archive file - :type archive_filename: string + :type archive_filename: six.text_type :raise ValueError: if the version requested is invalid """ # Read the file properties and its content - stat = os_stat(filename) + fil_stat = os_stat(filename) with open(filename, "rb") as fd: data = fd.read() @@ -544,9 +549,9 @@ def from_file(cls, filename, version=SAPCAR_VERSION_201, archive_filename=None): # Build the object and fill the fields archive_file = cls() archive_file._file_format = ff() - archive_file._file_format.perm_mode = stat.st_mode - archive_file._file_format.timestamp = stat.st_atime - archive_file._file_format.file_length = stat.st_size + archive_file._file_format.perm_mode = fil_stat.st_mode + archive_file._file_format.timestamp = fil_stat.st_atime + archive_file._file_format.file_length = fil_stat.st_size archive_file._file_format.filename = archive_filename archive_file._file_format.filename_length = len(archive_filename) if archive_file._file_format.version == SAPCAR_VERSION_201: @@ -568,7 +573,7 @@ def from_archive_file(cls, archive_file, version=SAPCAR_VERSION_201): :type archive_file: L{SAPCARArchiveFile} :param version: version of the file to construct - :type version: string + :type version: six.text_type :raise ValueError: if the version requested is invalid """ @@ -589,7 +594,7 @@ def from_archive_file(cls, archive_file, version=SAPCAR_VERSION_201): for block in archive_file._file_format.blocks: new_block = SAPCARCompressedBlockFormat() new_block.type = block.type - new_block.compressed = SAPCARCompressedBlobFormat(str(block.compressed)) + new_block.compressed = SAPCARCompressedBlobFormat(six.binary_type(block.compressed)) new_block.checksum = block.checksum new_archive_file._file_format.blocks.append(new_block) @@ -615,7 +620,7 @@ def open(self, enforce_checksum=False): raise Exception("Invalid file type") # Extract the file to a file-like object - out_file = StringIO() + out_file = BytesIO() checksum = self._file_format.extract(out_file) out_file.seek(0) @@ -653,13 +658,13 @@ def __init__(self, fil, mode="rb+", version=SAPCAR_VERSION_201): """Opens an archive file and allow access to it. :param fil: filename or file descriptor to open - :type fil: string or file + :type fil: six.text_type or file :param mode: mode to open the file - :type mode: string + :type mode: six.text_type :param version: archive file version to use when creating - :type version: string + :type version: six.text_type """ # Ensure version is withing supported versions @@ -674,7 +679,7 @@ def __init__(self, fil, mode="rb+", version=SAPCAR_VERSION_201): if "b" not in mode: mode += "b" - if isinstance(fil, (basestring, unicode)): + if isinstance(fil, six.text_type): self.filename = fil self.fd = open(fil, mode) else: @@ -691,13 +696,13 @@ def __init__(self, fil, mode="rb+", version=SAPCAR_VERSION_201): def files(self): """The list of file objects inside this archive file. - :return: list of file objects - :rtype: L{dict} of L{SAPCARArchiveFile} + :return: dictionary of file objects + :rtype: dict(six.text_type, SAPCARArchiveFile) """ fils = {} if self._files: for fil in self._files: - fils[fil.filename] = SAPCARArchiveFile(fil) + fils[fil.filename.decode()] = SAPCARArchiveFile(fil) return fils @property @@ -705,18 +710,21 @@ def files_names(self): """The list of file names inside this archive file. :return: list of file names - :rtype: L{list} of L{string} + :rtype: list(six.text_type) """ - return self.files.keys() + if six.PY2: + return self.files.keys() + # In Python 3, dict.keys() returns fancy new dict_keys object that needs type conversion + return list(self.files.keys()) @property def version(self): """The version of the archive file. :return: version - :rtype: string + :rtype: six.text_type """ - return self._sapcar.version + return unicode(self._sapcar.version) @version.setter def version(self, version): @@ -724,7 +732,7 @@ def version(self, version): converts the archive file. :param version: version to set - :type version: string + :type version: six.text_type """ if version not in sapcar_archive_file_versions: raise ValueError("Invalid version") @@ -749,7 +757,7 @@ def read(self): self._sapcar = SAPCARArchiveFormat(self.fd.read()) if self._sapcar.magic_string not in [SAPCAR_HEADER_MAGIC_STRING_STANDARD, SAPCAR_HEADER_MAGIC_STRING_BACKUP]: raise Exception("Invalid or unsupported magic string in file") - if self._sapcar.version not in sapcar_archive_file_versions: + if self._sapcar.version.decode() not in sapcar_archive_file_versions: raise Exception("Invalid or unsupported version in file") @property @@ -779,29 +787,29 @@ def write(self): """Writes the SAP CAR archive file to the file descriptor. """ self.fd.seek(0) - self.fd.write(str(self._sapcar)) + self.fd.write(six.binary_type(self._sapcar)) self.fd.flush() def write_as(self, filename=None): """Writes the SAP CAR archive file to another file. :param filename: name of the file to write to - :type filename: string + :type filename: six.text_type """ if not filename: self.write() else: - with open(filename, "w") as fd: - fd.write(str(self._sapcar)) + with open(filename, "wb") as fd: + fd.write(six.binary_type(self._sapcar)) def add_file(self, filename, archive_filename=None): """Adds a new file to the SAP CAR archive file. :param filename: name of the file to add - :type filename: string + :type filename: six.text_type :param archive_filename: name of the file to use in the archive - :type archive_filename: string + :type archive_filename: six.text_type """ fil = SAPCARArchiveFile.from_file(filename, self.version, archive_filename) self._files.append(fil._file_format) @@ -811,7 +819,7 @@ def open(self, filename): inside the SAP CAR archive. :param filename: name of the file to open - :type filename: string + :type filename: six.text_type :return: a file-like object that can be used to access the decompressed file. :rtype: file @@ -829,8 +837,8 @@ def raw(self): """Returns the raw data of the archive file. :return: raw data - :rtype: string + :rtype: six.binary_type """ if self._sapcar: - return str(self._sapcar) - return "" + return six.binary_type(self._sapcar) + return b"" diff --git a/pysap/SAPCredv2.py b/pysap/SAPCredv2.py index 8f763eb..94769aa 100644 --- a/pysap/SAPCredv2.py +++ b/pysap/SAPCredv2.py @@ -20,6 +20,7 @@ import logging from binascii import unhexlify # External imports +import six from scapy.packet import Packet from scapy.compat import plain_str from scapy.asn1packet import ASN1_Packet @@ -100,7 +101,7 @@ def decrypt_MSCryptProtect(plain, cred): entropy = cred.pse_path return dpapi_decrypt_blob(unhexlify(plain.blob.val), entropy) - PROVIDER_MSCryptProtect = "MSCryptProtect" + PROVIDER_MSCryptProtect = b"MSCryptProtect" """Provider for Windows hosts using DPAPI""" providers = { @@ -170,14 +171,14 @@ def lps_type_str(self): @property def cipher_format_version(self): cipher = self.cipher.val_readable - if len(cipher) >= 36 and ord(cipher[0]) in [0, 1]: - return ord(cipher[0]) + if len(cipher) >= 36 and six.byte2int(cipher) in [0, 1]: + return six.byte2int(cipher) return 0 @property def cipher_algorithm(self): if self.cipher_format_version == 1: - return ord(self.cipher.val_readable[1]) + return six.indexbytes(self.cipher.val_readable, 1) return 0 def decrypt(self, username): @@ -211,9 +212,9 @@ def decrypt_simple(self, username): blob = self.cipher.val_readable # Construct the key using the key format and the username - key = (cred_key_fmt % username)[:24] + key = six.b((cred_key_fmt % username)[:24]) # Set empty IV - iv = "\x00" * 8 + iv = b"\x00" * 8 # Decrypt the cipher text with the derived key and IV decryptor = Cipher(algorithms.TripleDES(key), modes.CBC(iv), backend=default_backend()).decryptor() @@ -241,7 +242,7 @@ def derive_key(self, key, blob, header, username): digest.update(blob[0:4]) digest.update(header.salt) digest.update(self.xor(username, ord(header.salt[0]))) - digest.update("" * 0x20) + digest.update(b"" * 0x20) hashed = digest.finalize() derived_key = self.xor(hashed, ord(header.salt[1])) @@ -340,7 +341,7 @@ def pse_file_path(self): @property def lps_type(self): - return ord(self.cipher.val_readable[1]) + return six.indexbytes(self.cipher.val_readable, 1) @property def lps_type_str(self): @@ -352,7 +353,7 @@ def lps_type_str(self): @property def cipher_format_version(self): - return ord(self.cipher.val_readable[0]) + return six.byte2int(self.cipher.val_readable) @property def cipher_algorithm(self): @@ -377,7 +378,7 @@ def decrypt(self, username=None): plain = cipher.decrypt() # Get the pin from the raw data - plain_size = ord(plain[0]) + plain_size = six.byte2int(plain) pin = plain[plain_size + 1:] # Create a plain credential container diff --git a/pysap/SAPHDB.py b/pysap/SAPHDB.py index 216887c..93621f1 100644 --- a/pysap/SAPHDB.py +++ b/pysap/SAPHDB.py @@ -21,6 +21,7 @@ import socket import struct # External imports +from six import b from cryptography.hazmat.backends import default_backend from scapy.layers.inet import TCP from scapy.packet import Packet, bind_layers, Raw @@ -805,7 +806,7 @@ class SAPHDBInitializationRequest(Packet): """ name = "SAP HANA SQL Command Network Protocol Initialization Request" fields_desc = [ - StrFixedLenField("initialization", "\xff\xff\xff\xff\x04\x20\x00\x04\x01\x00\x00\x01\x01\x01", 14), + StrFixedLenField("initialization", b("\xff\xff\xff\xff\x04\x20\x00\x04\x01\x00\x00\x01\x01\x01"), 14), ] @@ -854,7 +855,7 @@ def craft_authentication_request(self, value=None, connection=None): provided, it will include the Client Context part from it (e.g. application name). :param value: value to include in the authentication request - :type value: string + :type value: bytes :param connection: `HDB` connection to get client context values if required :type connection: :class:`SAPHDBConnection` @@ -864,7 +865,7 @@ def craft_authentication_request(self, value=None, connection=None): """ auth_fields = SAPHDBPartAuthentication(auth_fields=[SAPHDBPartAuthenticationField(value=self.username), SAPHDBPartAuthenticationField(value=self.METHOD), - SAPHDBPartAuthenticationField(value=value or "")]) + SAPHDBPartAuthenticationField(value=value or b(""))]) auth_part = SAPHDBPart(partkind=33, argumentcount=1, buffer=auth_fields) auth_segm = SAPHDBSegment(messagetype=65, parts=[auth_part]) @@ -880,14 +881,14 @@ def craft_authentication_response_part(self, auth_response_part, value=None): :type auth_response_part: :class:`SAPHDBPartAuthentication` :param value: value to include in the authentication response - :type value: string + :type value: bytes :return: the authentication response part :rtype: :class:`SAPHDBPart` """ auth_fields = SAPHDBPartAuthentication(auth_fields=[SAPHDBPartAuthenticationField(value=self.username), SAPHDBPartAuthenticationField(value=self.METHOD), - SAPHDBPartAuthenticationField(value=value or "")]) + SAPHDBPartAuthenticationField(value=value or b(""))]) return SAPHDBPart(partkind=33, argumentcount=1, buffer=auth_fields) def authenticate(self, connection): @@ -958,7 +959,7 @@ def obtain_client_proof(self, scram, client_key, auth_response_part): # Calculate the client proof from the password, salt and the server and client key # TODO: It might be good to see if this can be moved into a new Packet # TODO: We're only considering one server key - client_proof = b"\x00\x01" + struct.pack('b', scram.CLIENT_PROOF_SIZE) + client_proof = b("\x00\x01") + struct.pack('b', scram.CLIENT_PROOF_SIZE) client_proof += scram.scramble_salt(self.password, salt, server_key, client_key) return client_proof @@ -1004,7 +1005,7 @@ def obtain_client_proof(self, scram, client_key, auth_response_part): # Calculate the client proof from the password, salt, rounds and the server and client key # TODO: It might be good to see if this can be moved into a new Packet # TODO: We're only considering one server key - client_proof = b"\x00\x01" + struct.pack('b', scram.CLIENT_PROOF_SIZE) + client_proof = b("\x00\x01") + struct.pack('b', scram.CLIENT_PROOF_SIZE) client_proof += scram.scramble_salt(self.password, salt, server_key, client_key, rounds) return client_proof @@ -1089,7 +1090,7 @@ def craft_authentication_request(self, value=None, connection=None): doesn't use the standard ASN.1 encoding and instead leverage the same Authentication Field format. """ - auth_fields = SAPHDBPartAuthentication(auth_fields=[SAPHDBPartAuthenticationField(value=""), + auth_fields = SAPHDBPartAuthentication(auth_fields=[SAPHDBPartAuthenticationField(value=b("")), SAPHDBPartAuthenticationField(value=self.METHOD), SAPHDBPartAuthenticationField(value=value)]) auth_part = SAPHDBPart(partkind=33, argumentcount=1, buffer=auth_fields) @@ -1112,7 +1113,7 @@ def craft_authentication_response_part(self, auth_response_part, value=None): # gss_token_response = SAPHDBPartAuthentication(auth_response_part.auth_fields[1].value) last_gss_token = SAPHDBPartAuthentication(auth_fields=[SAPHDBPartAuthenticationField(value=self.krb5oid), - SAPHDBPartAuthenticationField(value="\x05")]) + SAPHDBPartAuthenticationField(value=b("\x05"))]) return super(SAPHDBAuthGSSMethod, self).craft_authentication_response_part(auth_response_part, last_gss_token) def authenticate(self, connection): @@ -1134,7 +1135,7 @@ def authenticate(self, connection): # * typeoid: type of the client GSS name # * gssname: the client GSS name (e.g. UPN) first_gss_token = SAPHDBPartAuthentication(auth_fields=[SAPHDBPartAuthenticationField(value=self.krb5oid), - SAPHDBPartAuthenticationField(value="\x01"), + SAPHDBPartAuthenticationField(value=b("\x01")), SAPHDBPartAuthenticationField(value=self.typeoid), SAPHDBPartAuthenticationField(value=self.username)]) first_auth_request = self.craft_authentication_request(first_gss_token, connection=connection) @@ -1174,7 +1175,7 @@ def authenticate(self, connection): # * commtype: communication type ("\x03") # * krb5ticket: the GSSAPI KRB5 AP-REQ structure to use second_gss_value = SAPHDBPartAuthentication(auth_fields=[SAPHDBPartAuthenticationField(value=self.krb5oid), - SAPHDBPartAuthenticationField(value="\x03"), + SAPHDBPartAuthenticationField(value=b("\x03")), SAPHDBPartAuthenticationField(value=krb5ticket)]) second_auth_request = self.craft_authentication_request(second_gss_value, connection=connection) second_auth_response = connection.sr(second_auth_request) @@ -1203,7 +1204,7 @@ def process_connect_response(self, connect_reponse, connection=None): # * commtype: communication type ("\x07") # * session cookie: the SessionCookie established for the connection gss_token = SAPHDBPartAuthentication(self.session_cookie) - if gss_token.auth_fields[1].value == "\x07": + if gss_token.auth_fields[1].value == b("\x07"): self.session_cookie = gss_token.auth_fields[2].value else: self.session_cookie = None @@ -1338,8 +1339,8 @@ def recv(self): header_raw = self._stream_socket.ins.recv(32) header = SAPHDB(header_raw) # Then get the payload - payload = "" - if header.varpartlength > 0: + payload = b("") + if header.varpartlength and header.varpartlength > 0: payload = self._stream_socket.ins.recv(header.varpartlength) # And finally construct the whole packet with header plus payload return SAPHDB(header_raw + payload) @@ -1358,7 +1359,7 @@ def initialize(self): self.send(init_request) # Receive initialization response packet - init_reply = SAPHDBInitializationReply(self._stream_socket.recv(8)) # We use the raw socket recv here + init_reply = SAPHDBInitializationReply(bytes(self._stream_socket.recv(8))) # We use the raw socket recv here self.product_version = init_reply.product_major self.protocol_version = init_reply.protocol_major diff --git a/pysap/SAPLPS.py b/pysap/SAPLPS.py index f5c288a..b7d6b93 100644 --- a/pysap/SAPLPS.py +++ b/pysap/SAPLPS.py @@ -35,7 +35,7 @@ log_lps = logging.getLogger("pysap.lps") -cred_key_lps_fallback = "\xe7\x6a\xd2\xce\x4b\xa7\xc7\x9e\xf9\x79\x5f\xa8\x2e\x6e\xaa\x1d\x76\x02\x2e\xcd\xd7\x74\x38\x51" +cred_key_lps_fallback = b"\xe7\x6a\xd2\xce\x4b\xa7\xc7\x9e\xf9\x79\x5f\xa8\x2e\x6e\xaa\x1d\x76\x02\x2e\xcd\xd7\x74\x38\x51" """Fixed key embedded in CommonCryptoLib for encrypted credentials using LPS in fallback mode""" @@ -90,7 +90,7 @@ def decrypt(self): Validation of the checksum and HMAC is not implemented. :return: decrypted object - :rtype: string + :rtype: bytes :raise NotImplementedError: if the LPS method is not implemented :raise SAP_LPS_Decryption_Error: if there's an error decrypting the object @@ -111,7 +111,7 @@ def decrypt(self): raise SAPLPSDecryptionError("Invalid LPS decryption method") # Decrypt the cipher text with the encryption key - iv = "\x00" * 16 + iv = b"\x00" * 16 decryptor = Cipher(algorithms.AES(encryption_key), modes.CBC(iv), backend=default_backend()).decryptor() plain = decryptor.update(self.encrypted_data) + decryptor.finalize() @@ -124,7 +124,7 @@ def decrypt_encryption_key_dpapi(self): the DP API without any additional entropy. :return: Encryption key decrypted - :rtype: string + :rtype: bytes """ log_lps.debug("Obtaining encryption key with DPAPI LPS mode") @@ -137,7 +137,7 @@ def decrypt_encryption_key_fallback(self): encrypt the actual encryption key used in the file with the AES cipher. :return: Encryption key decrypted - :rtype: string + :rtype: bytes """ log_lps.debug("Obtaining encryption key with FALLBACK LPS mode") @@ -149,7 +149,7 @@ def decrypt_encryption_key_fallback(self): hmac.update(self.context) default_key = hmac.finalize()[:16] - iv = "\x00" * 16 + iv = b"\x00" * 16 decryptor = Cipher(algorithms.AES(default_key), modes.CBC(iv), backend=default_backend()).decryptor() encryption_key = decryptor.update(self.encrypted_key) + decryptor.finalize() @@ -159,7 +159,7 @@ def decrypt_encryption_key_tpm(self): """Decrypts the encryption key using the TPM method. :return: Encryption key decrypted - :rtype: string + :rtype: bytes """ log_lps.error("LPS TPM decryption not implemented") raise NotImplementedError("LPS TPM decryption not implemented") diff --git a/pysap/SAPNI.py b/pysap/SAPNI.py index 856af47..11c4d1b 100644 --- a/pysap/SAPNI.py +++ b/pysap/SAPNI.py @@ -22,7 +22,7 @@ from select import select from struct import unpack from threading import Event -from SocketServer import BaseRequestHandler, ThreadingMixIn, TCPServer +from six.moves.socketserver import BaseRequestHandler, ThreadingMixIn, TCPServer # External imports from scapy.fields import LenField from scapy.packet import Packet, Raw @@ -58,13 +58,13 @@ class SAPNI(Packet): fields_desc = [LenField("length", None, fmt="!I")] # Constants for keep-alive messages - SAPNI_PING = "NI_PING\x00" + SAPNI_PING = b"NI_PING\x00" """ :cvar: Constant for keep-alive request messages (NI_PING) - :type: C{string} """ + :type: C{bytes} """ - SAPNI_PONG = "NI_PONG\x00" + SAPNI_PONG = b"NI_PONG\x00" """ :cvar: Constant for keep-alive response messages (NI_PONG) - :type: C{string} """ + :type: C{bytes} """ class SAPNIStreamSocket(StreamSocket): @@ -123,7 +123,7 @@ def recv(self): log_sapni.debug("Received 4 bytes NI header, to receive %d bytes data", nilength) # Receive the whole NI packet (length+payload) - nidata = '' + nidata = b'' while len(nidata) < nilength + 4: nidata += self.ins.recv(nilength - len(nidata) + 4) if len(nidata) == 0: @@ -472,8 +472,9 @@ def handle(self): self.handle_data() except socket.error as e: + errno, message = e.args log_sapni.debug("SAPNIServerHandler: Error handling data or client %s disconnected, %s (errno %d)", - self.client_address, e.message, e.errno) + self.client_address, message, errno) break def handle_data(self): diff --git a/pysap/SAPRouter.py b/pysap/SAPRouter.py index c276edb..a0c0cb4 100644 --- a/pysap/SAPRouter.py +++ b/pysap/SAPRouter.py @@ -33,6 +33,7 @@ from pysap.SAPSNC import SAPSNCFrame from pysap.SAPNI import (SAPNI, SAPNIStreamSocket, SAPNIProxy, SAPNIProxyHandler) +from pysap.utils import unicode from pysap.utils.fields import (PacketNoPadded, StrNullFixedLenField) @@ -184,11 +185,11 @@ def from_hops(cls, route_hops): """ result = "" for route_hop in route_hops: - result += "/H/{}".format(route_hop.hostname) + result += "/H/{}".format(unicode(route_hop.hostname)) if route_hop.port: - result += "/S/{}".format(route_hop.port) + result += "/S/{}".format(unicode(route_hop.port)) if route_hop.password: - result += "/W/{}".format(route_hop.password) + result += "/W/{}".format(unicode(route_hop.password)) return result @@ -394,25 +395,25 @@ class SAPRouter(Packet): SAPROUTER_DEFAULT_VERSION = 40 # Constants for router types - SAPROUTER_ROUTE = "NI_ROUTE" + SAPROUTER_ROUTE = b"NI_ROUTE" """ :cvar: Constant for route packets - :type: C{string} """ + :type: C{bytes} """ - SAPROUTER_ADMIN = "ROUTER_ADM" + SAPROUTER_ADMIN = b"ROUTER_ADM" """ :cvar: Constant for administration packets - :type: C{string} """ + :type: C{bytes} """ - SAPROUTER_ERROR = "NI_RTERR" + SAPROUTER_ERROR = b"NI_RTERR" """ :cvar: Constant for error information packets - :type: C{string} """ + :type: C{bytes} """ - SAPROUTER_CONTROL = "NI_RTERR" + SAPROUTER_CONTROL = b"NI_RTERR" """ :cvar: Constant for control messages packets - :type: C{string} """ + :type: C{bytes} """ - SAPROUTER_PONG = "NI_PONG" + SAPROUTER_PONG = b"NI_PONG" """ :cvar: Constant for route accepted packets - :type: C{string} """ + :type: C{bytes} """ router_type_values = [ SAPROUTER_ADMIN, @@ -422,7 +423,7 @@ class SAPRouter(Packet): SAPROUTER_PONG, ] """ :cvar: List of known packet types - :type: ``list`` of C{string} """ + :type: ``list`` of C{bytes} """ name = "SAP Router" fields_desc = [ @@ -447,7 +448,7 @@ class SAPRouter(Packet): ConditionalField(ShortField("adm_unused", 0x00), lambda pkt:router_is_admin(pkt) and pkt.adm_command not in [10, 11, 12, 13]), # Info Request fields - ConditionalField(StrNullFixedLenField("adm_password", "", 19), lambda pkt:router_is_admin(pkt) and pkt.adm_command in [2]), + ConditionalField(StrNullFixedLenField("adm_password", b"", 19), lambda pkt:router_is_admin(pkt) and pkt.adm_command in [2]), # Cancel Route fields ConditionalField(FieldLenField("adm_client_count", None, count_of="adm_client_ids", fmt="H"), lambda pkt:router_is_admin(pkt) and pkt.adm_command in [6]), @@ -472,7 +473,7 @@ class SAPRouter(Packet): # Control Message fields ConditionalField(IntField("control_text_length", 0), lambda pkt: router_is_control(pkt) and pkt.opcode != 0), - ConditionalField(StrField("control_text_value", "*ERR"), lambda pkt: router_is_control(pkt) and pkt.opcode != 0), + ConditionalField(StrField("control_text_value", b"*ERR"), lambda pkt: router_is_control(pkt) and pkt.opcode != 0), # SNC Frame fields ConditionalField(PacketField("snc_frame", None, SAPSNCFrame), lambda pkt: router_is_control(pkt) and pkt.opcode in [70, 71]) diff --git a/pysap/SAPSSFS.py b/pysap/SAPSSFS.py index 453e62d..c3af44f 100644 --- a/pysap/SAPSSFS.py +++ b/pysap/SAPSSFS.py @@ -19,6 +19,7 @@ # Standard imports import logging # External imports +from six import b from scapy.packet import Packet from scapy.fields import (ByteField, YesNoByteField, LenField, StrFixedLenField, StrField, PacketListField) from cryptography.exceptions import InvalidSignature @@ -34,7 +35,7 @@ log_ssfs = logging.getLogger("pysap.ssfs") -ssfs_hmac_key_unobscured = "\xe3\xa0\x61\x11\x85\x41\x68\x99\xf3\x0e\xda\x87\x7a\x80\xcc\x69" +ssfs_hmac_key_unobscured = b("\xe3\xa0\x61\x11\x85\x41\x68\x99\xf3\x0e\xda\x87\x7a\x80\xcc\x69") """Fixed key embedded in rsecssfx binaries for validating integrity of records""" @@ -58,8 +59,8 @@ class SAPSSFSLock(Packet): ByteField("file_type", 0), ByteField("type", 0), TimestampField("timestamp", None), - StrFixedLenPaddedField("user", None, 24, padd=" "), - StrFixedLenPaddedField("host", None, 24, padd=" "), + StrFixedLenPaddedField("user", None, 24, padd=b(" ")), + StrFixedLenPaddedField("host", None, 24, padd=b(" ")), ] @@ -74,8 +75,8 @@ class SAPSSFSKey(Packet): ByteField("type", 1), StrFixedLenField("key", None, 24), TimestampField("timestamp", None), - StrFixedLenPaddedField("user", None, 24, padd=" "), - StrFixedLenPaddedField("host", None, 24, padd=" "), + StrFixedLenPaddedField("user", None, 24, padd=b(" ")), + StrFixedLenPaddedField("host", None, 24, padd=b(" ")), ] @@ -99,7 +100,7 @@ class SAPSSFSDecryptedPayload(PacketNoPadded): @property def valid(self): """Returns whether the SHA1 value is valid for the given payload""" - blob = str(self) + blob = bytes(self) digest = Hash(SHA1(), backend=default_backend()) digest.update(blob[:8]) @@ -127,10 +128,10 @@ class SAPSSFSDataRecord(PacketNoPadded): ByteField("type", 1), # Record type "1" supported StrFixedLenField("filler1", None, 7), # Data Header - StrFixedLenPaddedField("key_name", None, 64, padd=" "), + StrFixedLenPaddedField("key_name", None, 64, padd=b(" ")), TimestampField("timestamp", None), - StrFixedLenPaddedField("user", None, 24, padd=" "), - StrFixedLenPaddedField("host", None, 24, padd=" "), + StrFixedLenPaddedField("user", None, 24, padd=b(" ")), + StrFixedLenPaddedField("host", None, 24, padd=b(" ")), YesNoByteField("is_deleted", 0), YesNoByteField("is_stored_as_plaintext", 0), YesNoByteField("is_binary_data", 0), @@ -151,16 +152,15 @@ def decrypt_data(self, key): log_ssfs.debug("Decrypting record {}".format(self.key_name)) decrypted_data = rsec_decrypt(self.data, key.key) decrypted_payload = SAPSSFSDecryptedPayload(decrypted_data) - log_ssfs.warn("Decrypted payload integrity is {}".format(decrypted_payload.valid)) + log_ssfs.warning("Decrypted payload integrity is {}".format(decrypted_payload.valid)) return decrypted_payload.data @property def valid(self): """Returns whether the HMAC-SHA1 value is valid for the given payload""" - # Calculate the HMAC-SHA1 h = HMAC(ssfs_hmac_key_unobscured, SHA1(), backend=default_backend()) - h.update(str(self)[24:156]) # Entire Data header without the HMAC field + h.update(bytes(self)[24:156]) # Entire Data header without the HMAC field h.update(self.data) # Validate the signature @@ -190,13 +190,13 @@ def has_record(self, key_name): """Returns if the data file contains a record with a given key name. :param key_name: the name of the key to look for - :type key_name: string + :type key_name: bytes :return: if the data file contains the record with key_name :rtype: bool """ for record in self.records: - if record.key_name.rstrip(" ") == key_name: + if record.key_name.rstrip(b(" ")) == key_name: return True return False @@ -204,20 +204,20 @@ def get_records(self, key_name): """Generator to retrieve records with the given key name. :param key_name: the name of the key to look for - :type key_name: string + :type key_name: bytes :return: the record with key_name :rtype: SAPSSFSDataRecord """ for record in self.records: - if record.key_name.rstrip(" ") == key_name: + if record.key_name.rstrip(b(" ")) == key_name: yield record def get_record(self, key_name): """Returns the first record with the given key name. :param key_name: the name of the key to look for - :type key_name: string + :type key_name: bytes :return: the record with key_name :rtype: SAPSSFSDataRecord @@ -228,7 +228,7 @@ def get_value(self, key_name, key=None): """Returns the value with the given key name. :param key_name: the name of the key to look for - :type key_name: string + :type key_name: bytes :param key: the encryption key :type key: SAPSSFSKey diff --git a/pysap/sapcarcli.py b/pysap/sapcarcli.py new file mode 100644 index 0000000..8da5c84 --- /dev/null +++ b/pysap/sapcarcli.py @@ -0,0 +1,342 @@ +#!/usr/bin/env python +# =========== +# pysap - Python library for crafting SAP's network protocols packets +# +# Copyright (C) 2012-2018 by Martin Gallo, Core Security +# +# The library was designed and developed by Martin Gallo from +# Core Security's CoreLabs team. +# +# 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 2 +# 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. +# ============== + +# Standard imports +import logging +from sys import stdin +from os import makedirs, utime, path +from os import sep as dir_separator +from argparse import ArgumentParser +# Custom imports +import pysap +from pysapcompress import DecompressError +from pysap.SAPCAR import SAPCARArchive, SAPCARInvalidChecksumException, SAPCARInvalidFileException + + +# Try to import OS-dependent functions +try: + from os import chmod, fchmod +except ImportError: + chmod = fchmod = None + + +pysapcar_usage = """ + +create archive with specified files: +pysapcar -c[v][f archive] [file1 file2 [/n filename] ...] + +list the contents of an archive: +pysapcar -t[v][f archive] [file1 file2 ...] + +extract files from an archive: +pysapcar -x[v][f archive] [-o outdir] [file1 file2 ...] + +append files to an archive: +pysapcar -a[v][f archive] [file1 file2 [/n filename] ...] + +""" + + +class PySAPCAR(object): + + def __init__(self): + # Private attributes + self._logger = None + + # Instance attributes + self.mode = None + self.verbose = False + self.archive_fd = None + + @staticmethod + def parse_options(): + """Parses command-line options. + """ + description = "Basic and experimental implementation of SAPCAR archive format." + + parser = ArgumentParser(usage=pysapcar_usage, description=description, epilog=pysap.epilog) + parser.add_argument("--version", action="version", version="%(prog)s {}".format(pysap.__version__)) + + # Commands + parser.add_argument("-c", dest="create", action="store_true", help="Create archive with specified files") + parser.add_argument("-x", dest="extract", action="store_true", help="Extract files from an archive") + parser.add_argument("-t", dest="list", action="store_true", help="List the contents of an archive") + parser.add_argument("-a", dest="append", action="store_true", help="Append files to an archive") + parser.add_argument("-f", dest="filename", help="Archive filename", metavar="FILE") + parser.add_argument("-o", dest="outdir", help="Path to directory where to extract files") + + misc = parser.add_argument_group("Misc options") + misc.add_argument("-v", "--verbose", dest="verbose", action="store_true", help="Verbose output") + misc.add_argument("-e", "--enforce-checksum", dest="enforce_checksum", action="store_true", + help="Whether the checksum validation is enforced. A file with an invalid checksum won't be " + "extracted. When not set, only a warning would be thrown if checksum is invalid.") + misc.add_argument("-b", "--break-on-error", dest="break_on_error", action="store_true", + help="Whether the extraction would continue if an error is identified.") + + (options, args) = parser.parse_known_args() + + return options, args + + @property + def logger(self): + """Sets the logger of the cli tool. + """ + if self._logger is None: + self._logger = logging.getLogger("pysap.pysapcar") + self._logger.setLevel(logging.DEBUG if self.verbose else logging.INFO) + self._logger.addHandler(logging.StreamHandler()) + return self._logger + + def main(self): + """Main routine for parsing options and dispatch desired action. + """ + options, args = self.parse_options() + + # Set the verbosity + self.verbose = options.verbose + + self.logger.info("pysapcar version: %s", pysap.__version__) + + # Check the mode the archive file should be opened + if options.list or options.extract: + self.mode = "r" + elif options.create: + self.mode = "w" + elif options.append: + self.mode = "r+" + else: # default to read mode + self.mode = "r" + + # Opens the input/output file + self.archive_fd = None + if options.filename: + try: + self.archive_fd = open(options.filename, self.mode) + except IOError as e: + self.logger.error("pysapcar: error opening '%s' (%s)" % (options.filename, str(e))) + return + else: + self.archive_fd = stdin + + # Execute the action + try: + if options.create or options.append: + self.append(options, args) + elif options.list: + self.list(options, args) + elif options.extract: + self.extract(options, args) + finally: + self.archive_fd.close() + + def open_archive(self): + """Opens the archive file to work with it and returns + the SAP Car Archive object. + """ + try: + sapcar = SAPCARArchive(self.archive_fd, mode=self.mode) + self.logger.info("pysapcar: Processing archive '%s' (version %s)", self.archive_fd.name, sapcar.version) + except Exception as e: + self.logger.error("pysapcar: Error processing archive '%s' (%s)", self.archive_fd, str(e)) + return None + return sapcar + + @staticmethod + def target_files(filenames, target_filenames=None): + """Generates the list of files to work on. It calculates + the intersection between the file names selected in + command-line and the ones in the archive to work on. + + :param filenames: filenames in the archive + :param target_filenames: filenames to work on + + :return: filename + """ + files = set(filenames) + if target_filenames: + files = files.intersection(set(target_filenames)) + + for filename in sorted(files): # Ensure iteration order + yield filename + + def append(self, options, args): + """Appends a file to the archive file. + """ + + if len(args) < 1: + self.logger.error("pysapcar: no files specified for appending") + return + + # Open the archive file + sapcar = self.open_archive() + if not sapcar: + return + + while len(args): + filename = filename_in_archive = args.pop(0) + + self.logger.debug(args) + if len(args) >= 2 and args[0] == "/n": + args.pop(0) + filename_in_archive = args.pop(0) + + sapcar.add_file(filename, archive_filename=filename_in_archive) + if filename != filename_in_archive: + self.logger.info("d %s (original name %s)", filename_in_archive, filename) + else: + self.logger.info("d %s", filename) + + sapcar.write() + + def list(self, options, args): + """List files inside the archive file and print their + attributes: permissions, size, timestamp and filename. + """ + # Open the archive file + sapcar = self.open_archive() + if not sapcar: + return + # Print the info of each file + for filename in self.target_files(sapcar.files_names, args): + fil = sapcar.files[filename] + self.logger.info("{} {:>10} {} {}".format(fil.permissions, fil.size, fil.timestamp, fil.filename)) + + def extract(self, options, args): + """Extract files from the archive file. + """ + CONTINUE = 1 + SKIP = 2 + STOP = 3 + + # Open the archive file + sapcar = self.open_archive() + if not sapcar: + return + + # Warn if permissions can't be set + if not chmod: + self.logger.warning("pysapcar: Setting extracted files permissions not implemented in this platform") + + # Extract each file in the archive + no = 0 + for filename in self.target_files(sapcar.files_names, args): + flag = CONTINUE + fil = sapcar.files[filename] + filename = path.normpath(filename.replace("\x00", "")) # Take out null bytes if found + if options.outdir: + # Have to strip directory separator from the beginning of the file name, because path.join disregards + # all previous components if any of the following components is an absolute path + filename = path.join(path.normpath(options.outdir), filename.lstrip(dir_separator)) + + if fil.is_directory(): + try: + makedirs(filename) + except (IOError, OSError) as e: + # errno 17 == File exists + if e.errno != 17: + self.logger.error("pysapcar: Could not create directory '%s' (%s)", filename, e) + if options.break_on_error: + self.logger.info("pysapcar: Stopping extraction") + break + self.logger.info("pysapcar: Skipping extraction of directory '%s'", filename) + continue + else: + # No need to check for errors on chmod and utime: makedirs would've already raised + if chmod: + chmod(filename, fil.perm_mode) + utime(filename, (fil.timestamp_raw, fil.timestamp_raw)) + self.logger.info("d %s", filename) + no += 1 + + elif fil.is_file(): + # If the file references a directory that is not there, create it first + file_dirname = path.dirname(filename) + if file_dirname and not path.exists(file_dirname): + # mkdir barfs if archive contains /foo/bar/bash but not /foo/bar directory. + # makedirs creates intermediate directories as well + try: + makedirs(file_dirname) + self.logger.info("d %s", file_dirname) + except (IOError, OSError) as e: + # errno 17 == File exists + if e.errno != 17: + self.logger.error("pysapcar: Could not create intermediate directory '%s' for '%s' (%s)", + file_dirname, filename, e.strerror) + if options.break_on_error: + flag = STOP + else: + flag = SKIP + + # Try to extract the file and handle potential errors + try: + data = fil.open(enforce_checksum=options.enforce_checksum).read() + except (SAPCARInvalidFileException, DecompressError) as e: + self.logger.error("pysapcar: Invalid SAP CAR file '%s' (%s)", self.archive_fd.name, str(e)) + if options.break_on_error: + flag = STOP + else: + flag = SKIP + except SAPCARInvalidChecksumException: + if options.enforce_checksum: + self.logger.error("pysapcar: Invalid checksum found for file '%s'", fil.filename) + flag = STOP + else: + self.logger.warning("pysapcar: checksum error in '%s' !", filename) + + # Check the result before starting to write the file + if flag == SKIP: + self.logger.info("pysapcar: Skipping extraction of file '%s'", fil.filename) + continue + elif flag == STOP: + self.logger.info("pysapcar: Stopping extraction") + break + + # Write the new file and set permissions + try: + with open(filename, "wb") as new_file: + new_file.write(data) + if fchmod: + fchmod(new_file.fileno(), fil.perm_mode) + # Set the timestamp + utime(filename, (fil.timestamp_raw, fil.timestamp_raw)) + except (IOError, OSError) as e: + self.logger.error("pysapcar: Failed to extract file '%s', (%s)", filename, str(e)) + if options.break_on_error: + self.logger.info("pysapcar: Stopping extraction") + break + + # Set the timestamp + utime(filename, (fil.timestamp_raw, fil.timestamp_raw)) + + # If this path is reached and checksum is not valid, means checksum is not enforced, so we should warn + # only. + if not fil.check_checksum(): + self.logger.warning("pysapcar: checksum error in '%s' !", filename) + + self.logger.info("d %s", filename) + no += 1 + else: + self.logger.warning("pysapcar: Invalid file type '%s'", filename) + + self.logger.info("pysapcar: %d file(s) processed", no) + + +if __name__ == "__main__": + PySAPCAR().main() diff --git a/pysap/utils/__init__.py b/pysap/utils/__init__.py index 2575143..ebffcc0 100644 --- a/pysap/utils/__init__.py +++ b/pysap/utils/__init__.py @@ -17,8 +17,9 @@ # # Standard imports -from Queue import Queue +from six.moves import queue from threading import Thread, Event +from six import binary_type, text_type class Worker(Thread): @@ -65,7 +66,7 @@ def run(self): class ThreadPool(object): """Pool of threads consuming tasks from a queue""" def __init__(self, num_threads): - self.tasks = Queue(num_threads) + self.tasks = queue(num_threads) for _ in range(num_threads): WorkerQueue(self.tasks) @@ -76,3 +77,21 @@ def add_task(self, func, *args, **kargs): def wait_completion(self): """Wait for completion of all the tasks in the queue""" self.tasks.join() + + +# All custom general purpose Python 2/3 compatibility should go here + + +def unicode(string): + """Convert given string to unicode string + + :param string: String to convert + :type string: bytes | str | unicode + :return: six.text_type + """ + string_type = type(string) + if string_type == binary_type: + return string.decode() + elif string_type == text_type: + return string + raise ValueError("Expected bytes or str, got {}".format(string_type)) \ No newline at end of file diff --git a/pysap/utils/crypto/__init__.py b/pysap/utils/crypto/__init__.py index 5a1d2cc..a42f435 100644 --- a/pysap/utils/crypto/__init__.py +++ b/pysap/utils/crypto/__init__.py @@ -20,8 +20,10 @@ import os import math # Custom imports -from rsec import RSECCipher +from .rsec import RSECCipher # External imports +from six import b +from six.moves import xrange from cryptography.exceptions import InvalidKey from cryptography.hazmat.primitives.hmac import HMAC from cryptography.hazmat.primitives.ciphers import Cipher @@ -45,7 +47,7 @@ def dpapi_decrypt_blob(blob, entropy=None): :type entropy: string :return: decrypted blob - :rtype: string + :rtype: bytes :raise Exception: if the platform is not Windows or the decryption failed """ @@ -145,7 +147,7 @@ def digest(inp): def to_int(value): if value == b'': return 0 - return long(value.encode("hex"), 16) + return int(value.encode("hex"), 16) def to_bytes(value): value = "%x" % value @@ -156,7 +158,7 @@ def to_bytes(value): a = b'\x00' * (c * u) for n in range(1, c + 1): - a2 = digest(d + i) + a2 = digest(d.encode('latin-1') + i) for _ in range(2, self._iterations + 1): a2 = digest(a2) @@ -166,7 +168,7 @@ def to_bytes(value): while len(b) < v: b += a2 - b = to_int(b[:v]) + 1 + b = to_int(str(b)[:v]) + 1 # Step 6.C for n2 in range(0, len(i) // v): @@ -278,7 +280,7 @@ def get_client_key(self): return os.urandom(self.CLIENT_KEY_SIZE) def salt_key(self, password, salt, rounds): - hmac = HMAC(password, self.ALGORITHM(), self.backend) + hmac = HMAC(b(password), self.ALGORITHM(), self.backend) hmac.update(salt) return hmac.finalize() @@ -331,7 +333,7 @@ class SCRAM_PBKDF2SHA256(SCRAM_SHA256): def salt_key(self, password, salt, rounds): pbkdf2 = PBKDF2HMAC(self.ALGORITHM(), self.CLIENT_PROOF_SIZE, salt, rounds, self.backend) - return pbkdf2.derive(password) + return pbkdf2.derive(b(password)) def rsec_decrypt(blob, key): @@ -355,8 +357,14 @@ def rsec_decrypt(blob, key): if len(key) != 24: raise Exception("Wrong key length") - blob = [ord(i) for i in blob] - key = [ord(i) for i in key] + try: + blob = [ord(i) for i in blob] + except TypeError: + blob = [i for i in blob] + try: + key = [ord(i) for i in key] + except TypeError: + key = [i for i in key] key1 = key[0:8] key2 = key[8:16] key3 = key[16:24] diff --git a/pysapcompress/pysapcompress.cpp b/pysapcompress/pysapcompress.cpp index b13e3a4..17707bc 100644 --- a/pysapcompress/pysapcompress.cpp +++ b/pysapcompress/pysapcompress.cpp @@ -19,12 +19,16 @@ # */ +/* Python 3.10 requires PY_SSIZE_T_CLEAN macro when using PyArg_ParseTuple() with "s#" format */ +#define PY_SSIZE_T_CLEAN + #include #include #include #include #include +#include #include "hpa101saptype.h" #include "hpa104CsObject.h" @@ -32,6 +36,13 @@ #include "hpa107cslzh.h" #include "hpa105CsObjInt.h" +/* compress and decompress take and return bytes on Py3, unfortunately there's no way to enforce it on Py2 */ +#if PY_MAJOR_VERSION >= 3 + #define BYTES_FORMAT "y#" +#else + #define BYTES_FORMAT "s#" +#endif + /* Memory allocation factor constant */ #define MEMORY_ALLOC_FACTOR 10 @@ -364,7 +375,7 @@ static char pysapcompress_compress_doc[] = "Compress a buffer using SAP's compre ":param str in: input buffer to compress\n\n" ":param int algorithm: algorithm to use\n\n" ":return: tuple with return code, output length and output buffer\n" - ":rtype: tuple of int, int, string\n\n" + ":rtype: tuple of int, int, bytes\n\n" ":raises CompressError: if an error occurred during compression\n"; static PyObject * @@ -373,6 +384,7 @@ pysapcompress_compress(PyObject *self, PyObject *args, PyObject *keywds) const unsigned char *in = NULL; unsigned char *out = NULL; int status = 0, in_length = 0, out_length = 0, algorithm = ALG_LZC; + Py_ssize_t in_length_arg = 0; /* Define the keyword list */ static char kwin[] = "in"; @@ -380,9 +392,16 @@ pysapcompress_compress(PyObject *self, PyObject *args, PyObject *keywds) static char* kwlist[] = {kwin, kwalgorithm, NULL}; /* Parse the parameters. We are also interested in the length of the input buffer. */ - if (!PyArg_ParseTupleAndKeywords(args, keywds, "s#|i", kwlist, &in, &in_length, &algorithm)) + if (!PyArg_ParseTupleAndKeywords(args, keywds, BYTES_FORMAT"|i", kwlist, &in, &in_length_arg, &algorithm)) return (NULL); + /* Check the size of length args and convert from Py_ssize_t to int */ + if (in_length_arg > INT_MAX) { + return (PyErr_Format(compression_exception, "Compression error (Input length is larger than INT_MAX)")); + } + + in_length = Py_SAFE_DOWNCAST(in_length_arg, Py_ssize_t, int); + /* Call the compression function */ status = compress_packet(in, in_length, &out, &out_length, algorithm); @@ -398,7 +417,7 @@ pysapcompress_compress(PyObject *self, PyObject *args, PyObject *keywds) } /* It no error was raised, return the compressed buffer and the length */ - return (Py_BuildValue("iis#", status, out_length, out, out_length)); + return (Py_BuildValue("ii"BYTES_FORMAT, status, out_length, out, out_length)); } @@ -407,7 +426,7 @@ static char pysapcompress_decompress_doc[] = "Decompress a buffer using SAP's co ":param str in: input buffer to decompress\n" ":param int out_length: length of the output to decompress\n" ":return: tuple of return code, output length and output buffer\n" - ":rtype: tuple of int, int, string\n\n" + ":rtype: tuple of int, int, bytes\n\n" ":raises DecompressError: if an error occurred during decompression\n"; static PyObject * @@ -416,11 +435,21 @@ pysapcompress_decompress(PyObject *self, PyObject *args) const unsigned char *in = NULL; unsigned char *out = NULL; int status = 0, in_length = 0, out_length = 0; + Py_ssize_t in_length_arg = 0, out_length_arg = 0; /* Parse the parameters. We are also interested in the length of the input buffer. */ - if (!PyArg_ParseTuple(args, "s#i", &in, &in_length, &out_length)) + if (!PyArg_ParseTuple(args, BYTES_FORMAT"i", &in, &in_length_arg, &out_length_arg)) return (NULL); + /* Check the size of length args and convert from Py_ssize_t to int */ + if (in_length_arg > INT_MAX) + return (PyErr_Format(decompression_exception, "Decompression error (Input length is larger than INT_MAX)")); + if (out_length_arg > INT_MAX) + return (PyErr_Format(decompression_exception, "Decompression error (Output length is larger than INT_MAX)")); + + in_length = Py_SAFE_DOWNCAST(in_length_arg, Py_ssize_t, int); + out_length = Py_SAFE_DOWNCAST(out_length_arg, Py_ssize_t, int); + /* Call the compression function */ status = decompress_packet(in, in_length, &out, &out_length); @@ -434,7 +463,7 @@ pysapcompress_decompress(PyObject *self, PyObject *args) return (PyErr_Format(decompression_exception, "Decompression error (%s)", error_string(status))); } /* It no error was raised, return the uncompressed buffer and the length */ - return (Py_BuildValue("iis#", status, out_length, out, out_length)); + return (Py_BuildValue("ii"BYTES_FORMAT, status, out_length, out, out_length)); } @@ -450,12 +479,31 @@ static PyMethodDef pysapcompressMethods[] = { static char pysapcompress_module_doc[] = "Library implementing SAP's LZH and LZC compression algorithms."; /* Module initialization */ -PyMODINIT_FUNC -initpysapcompress(void) +/* Python 2 and 3 compatiblitily shenanigans, from http://python3porting.com/cextensions.html */ +#if PY_MAJOR_VERSION >= 3 + #define MOD_ERROR_VAL NULL + #define MOD_SUCCESS_VAL(val) val + #define MOD_INIT(name) PyMODINIT_FUNC PyInit_##name(void) + #define MOD_DEF(ob, name, doc, methods) \ + static struct PyModuleDef moduledef = { \ + PyModuleDef_HEAD_INIT, name, doc, -1, methods, }; \ + ob = PyModule_Create(&moduledef); +#else + #define MOD_ERROR_VAL + #define MOD_SUCCESS_VAL(val) + #define MOD_INIT(name) PyMODINIT_FUNC init##name(void) + #define MOD_DEF(ob, name, doc, methods) \ + ob = Py_InitModule3(name, methods, doc); +#endif + +MOD_INIT(pysapcompress) { PyObject *module = NULL; /* Create the module and define the methods */ - module = Py_InitModule3("pysapcompress", pysapcompressMethods, pysapcompress_module_doc); + MOD_DEF(module, "pysapcompress", pysapcompress_module_doc, pysapcompressMethods) + + if (module == NULL) + return MOD_ERROR_VAL; /* Add the algorithm constants */ PyModule_AddIntConstant(module, "ALG_LZC", ALG_LZC); @@ -468,4 +516,5 @@ initpysapcompress(void) decompression_exception = PyErr_NewException(decompression_exception_name, NULL, NULL); PyModule_AddObject(module, decompression_exception_short, decompression_exception); + return MOD_SUCCESS_VAL(module); } diff --git a/requirements-docs.txt b/requirements-docs.txt index da2a1fc..52dccfa 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,7 @@ Sphinx==1.8.5 ipykernel -nbsphinx==0.5.1 -pyx==0.12.1 +nbsphinx +pyx==0.12.1; python_version < '3.2' +pyx==0.14.1; python_version >= '3.2' ipython<6.0 m2r==0.2.1 diff --git a/requirements-tests.txt b/requirements-tests.txt new file mode 100644 index 0000000..6860787 --- /dev/null +++ b/requirements-tests.txt @@ -0,0 +1,4 @@ +testfixtures==6.17.1 +mock==3.0.5; python_version < '3' +tox==3.23.1 +pytest \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index d049cba..6faa008 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ +six==1.15 scapy==2.4.4 cryptography==2.9.2 diff --git a/setup.py b/setup.py index d773cd9..873f103 100755 --- a/setup.py +++ b/setup.py @@ -104,6 +104,7 @@ def run(self): url=pysap.__url__, download_url=pysap.__url__, license=pysap.__license__, + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4', classifiers=['Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'Intended Audience :: Information Technology', @@ -122,9 +123,6 @@ def run(self): # Script files scripts=['bin/pysapcar', 'bin/pysapgenpse'], - # Tests command - test_suite='tests.test_suite', - # Documentation commands cmdclass={'doc': DocumentationCommand, 'notebooks': PreExecuteNotebooksCommand}, diff --git a/tests/__init__.py b/tests/__init__.py old mode 100755 new mode 100644 index 763d2ce..ccbe1b2 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -21,7 +21,7 @@ import unittest -test_suite = unittest.defaultTestLoader.discover('.', '*_test.py') +test_suite = unittest.defaultTestLoader.discover('.', 'test_*.py') if __name__ == '__main__': diff --git a/tests/crypto_test.py b/tests/test_crypto.py old mode 100644 new mode 100755 similarity index 100% rename from tests/crypto_test.py rename to tests/test_crypto.py diff --git a/tests/pysapcompress_test.py b/tests/test_pysapcompress.py similarity index 87% rename from tests/pysapcompress_test.py rename to tests/test_pysapcompress.py index ff25e4f..c2ace8a 100755 --- a/tests/pysapcompress_test.py +++ b/tests/test_pysapcompress.py @@ -17,39 +17,42 @@ # # Standard imports +from __future__ import unicode_literals import sys import unittest +# External imports +from six import assertRaisesRegex, text_type # Custom imports from tests.utils import read_data_file class PySAPCompressTest(unittest.TestCase): - test_string_plain = "TEST" * 70 - test_string_compr_lzc = '\x18\x01\x00\x00\x11\x1f\x9d\x8dT\x8aL\xa1\x12p`A\x82\x02\x11\x1aLx\xb0!\xc3\x87\x0b#*\x9c\xe8' \ - 'PbE\x8a\x101Z\xccx\xb1#\xc7\x8f\x1bCj\x1c\xe9QdI\x92 Q\x9aLy\xf2 ' - test_string_compr_lzh = '\x18\x01\x00\x00\x12\x1f\x9d\x02]\x88kpH\xc8(\xc6\xc0\x00\x00' + test_string_plain = b"TEST" * 70 + test_string_compr_lzc = b'\x18\x01\x00\x00\x11\x1f\x9d\x8dT\x8aL\xa1\x12p`A\x82\x02\x11\x1aLx\xb0!\xc3\x87\x0b#*\x9c\xe8' \ + b'PbE\x8a\x101Z\xccx\xb1#\xc7\x8f\x1bCj\x1c\xe9QdI\x92 Q\x9aLy\xf2 ' + test_string_compr_lzh = b'\x18\x01\x00\x00\x12\x1f\x9d\x02]\x88kpH\xc8(\xc6\xc0\x00\x00' def test_import(self): """Test import of the pysapcompress library""" try: import pysapcompress # @UnusedImport # noqa: F401 except ImportError as e: - self.Fail(str(e)) + self.fail(text_type(e)) def test_compress_input(self): """Test compress function input""" from pysapcompress import compress, CompressError - self.assertRaisesRegexp(CompressError, "invalid input length", compress, "") - self.assertRaisesRegexp(CompressError, "unknown algorithm", compress, "TestString", algorithm=999) + assertRaisesRegex(self, CompressError, "invalid input length", compress, b"") + assertRaisesRegex(self, CompressError, "unknown algorithm", compress, b"TestString", algorithm=999) def test_decompress_input(self): """Test decompress function input""" from pysapcompress import decompress, DecompressError - self.assertRaisesRegexp(DecompressError, "invalid input length", decompress, "", 1) - self.assertRaisesRegexp(DecompressError, "input not compressed", decompress, "AAAAAAAA", 1) - self.assertRaisesRegexp(DecompressError, "unknown algorithm", decompress, - "\x0f\x00\x00\x00\xff\x1f\x9d\x00\x00\x00\x00", 1) + assertRaisesRegex(self, DecompressError, "invalid input length", decompress, b"", 1) + assertRaisesRegex(self, DecompressError, "input not compressed", decompress, b"AAAAAAAA", 1) + assertRaisesRegex(self, DecompressError, "unknown algorithm", decompress, + b"\x0f\x00\x00\x00\xff\x1f\x9d\x00\x00\x00\x00", 1) def test_lzc(self): """Test compression and decompression using LZC algorithm""" diff --git a/tests/sapcar_test.py b/tests/test_sapcar.py similarity index 92% rename from tests/sapcar_test.py rename to tests/test_sapcar.py index c2173f9..766fc2a 100755 --- a/tests/sapcar_test.py +++ b/tests/test_sapcar.py @@ -17,10 +17,11 @@ # # Standard imports +from __future__ import unicode_literals +import six import sys import unittest -from os import unlink, rmdir -from os.path import basename, exists +from os import unlink, rmdir, path # External imports # Custom imports from tests.utils import data_filename @@ -36,7 +37,7 @@ class PySAPCARTest(unittest.TestCase): test_timestamp = "01 Dec 2015 22:48" test_perm_mode = 33204 test_permissions = "-rw-rw-r--" - test_string = "The quick brown fox jumps over the lazy dog" + test_string = b"The quick brown fox jumps over the lazy dog" def setUp(self): with open(self.test_filename, "wb") as fd: @@ -44,9 +45,11 @@ def setUp(self): def tearDown(self): for filename in [self.test_filename, self.test_archive_file]: - if exists(filename): + if path.exists(filename): unlink(filename) - if exists("test"): + if path.exists(path.join("test", "blah")): + unlink(path.join("test", "blah")) + if path.exists("test"): rmdir("test") def check_sapcar_archive(self, filename, version): @@ -55,12 +58,15 @@ def check_sapcar_archive(self, filename, version): with open(data_filename(filename), "rb") as fd: sapcar_archive = SAPCARArchive(fd, mode="r") - self.assertEqual(filename, basename(sapcar_archive.filename)) + self.assertEqual(filename, path.basename(sapcar_archive.filename)) self.assertEqual(version, sapcar_archive.version) self.assertEqual(1, len(sapcar_archive.files)) self.assertEqual(1, len(sapcar_archive.files_names)) self.assertListEqual([self.test_filename], sapcar_archive.files_names) - self.assertListEqual([self.test_filename], sapcar_archive.files.keys()) + if six.PY2: + self.assertListEqual([self.test_filename], sapcar_archive.files.keys()) + else: + self.assertListEqual([self.test_filename], list(sapcar_archive.files.keys())) af = sapcar_archive.open(self.test_filename) self.assertEqual(self.test_string, af.read()) @@ -111,7 +117,10 @@ def test_sapcar_archive_add_file(self): self.assertEqual(2, len(ar.files)) self.assertEqual(2, len(ar.files_names)) self.assertListEqual([self.test_filename, self.test_filename+"two"], ar.files_names) - self.assertListEqual([self.test_filename, self.test_filename+"two"], ar.files.keys()) + if six.PY2: + self.assertListEqual([self.test_filename, self.test_filename+"two"], ar.files.keys()) + else: + self.assertListEqual([self.test_filename, self.test_filename+"two"], list(ar.files.keys())) for filename in [self.test_filename, self.test_filename+"two"]: af = ar.open(filename) diff --git a/tests/test_sapcarcli.py b/tests/test_sapcarcli.py new file mode 100755 index 0000000..8e9bf9b --- /dev/null +++ b/tests/test_sapcarcli.py @@ -0,0 +1,377 @@ +# =========== +# pysap - Python library for crafting SAP's network protocols packets +# +# Copyright (C) 2012-2018 by Martin Gallo, Core Security +# +# The library was designed and developed by Martin Gallo from +# Core Security's CoreLabs team. +# +# 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 2 +# 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. +# ============== + +# Standard imports +from __future__ import unicode_literals, absolute_import +import unittest +from os import path +try: + from unittest import mock +except ImportError: + import mock +# External imports +from testfixtures import LogCapture +# Custom imports +from tests.utils import data_filename +from pysap.sapcarcli import PySAPCAR +from pysap.SAPCAR import SAPCARArchive, SAPCARArchiveFile, SAPCARInvalidFileException, SAPCARInvalidChecksumException + + +class PySAPCARCLITest(unittest.TestCase): + + def setUp(self): + self.mock_file = mock.Mock( + spec=SAPCARArchiveFile, + filename=path.join("test", "bl\x00ah"), + is_directory=lambda: False, + is_file=lambda: True, + perm_mode="-rw-rw-r--", + timestamp_raw=7 + ) + self.mock_dir = mock.Mock( + spec=SAPCARArchiveFile, + filename=path.basename(self.mock_file.filename), + is_directory=lambda: True, + is_file=lambda: False, + perm_mode="-rwxrw-r--", + timestamp_raw=8 + ) + self.mock_archive = mock.Mock( + spec=SAPCARArchive, + files={ + self.mock_file.filename: self.mock_file, + self.mock_dir.filename: self.mock_dir + } + ) + self.cli = PySAPCAR() + self.cli.mode = "r" + self.cli.archive_fd = open(data_filename("car201_test_string.sar"), "rb") + self.cli.logger.handlers = [] # Mute logging while running unit tests + self.log = LogCapture() # Enable logger output checking + + def tearDown(self): + try: + self.cli.archive_fd.close() + except Exception: + pass + self.log.uninstall() + + def test_open_archive_fd_not_set(self): + temp = self.cli.archive_fd # Backup test archive file handle + self.cli.archive_fd = None + self.assertIsNone(self.cli.open_archive()) + self.cli.archive_fd = temp + + def test_open_archive_raises_exception(self): + # While SAPCARArchive is defined in pysap.SAPCAR, it's looked up in sapcarcli, so we need to patch it there + # See https://docs.python.org/3/library/unittest.mock.html#where-to-patch for details + with mock.patch("pysap.sapcarcli.SAPCARArchive") as mock_archive: + mock_archive.side_effect = Exception("unit test exception") + self.assertIsNone(self.cli.open_archive()) + mock_archive.assert_called_once_with(self.cli.archive_fd, mode=self.cli.mode) + + def test_open_archive_succeeds(self): + self.assertIsInstance(self.cli.open_archive(), SAPCARArchive) + + def test_target_files_no_kwargs(self): + names = sorted(["list", "of", "test", "names"]) + self.assertEqual(names, [n for n in self.cli.target_files(names)]) + + def test_target_files_with_kwargs(self): + names = sorted(["list", "of", "test", "names"]) + targets = sorted(["list", "names", "blah"]) + self.assertEqual(["list", "names"], [n for n in self.cli.target_files(names, targets)]) + + def test_list_open_fails(self): + with mock.patch.object(self.cli, "open_archive", return_value=None): + self.assertIsNone(self.cli.list(None, None)) + + def test_list_succeeds(self): + archive_attrs = { + "files_names": ["test_string.txt", "blah.so"], + "files": { + "test_string.txt": mock.MagicMock(**{ + "permissions": "-rw-rw-r--", + "size": 43, + "timestamp": "01 Dec 2015 22:48", + "filename": "test_string.txt" + }), + "blah.so": mock.MagicMock(**{ + "permissions": "-rwxrwxr-x", + "size": 1243, + "timestamp": "01 Dec 2017 13:37", + "filename": "blah.so" + }) + } + } + mock_archive = mock.MagicMock(**archive_attrs) + with mock.patch.object(self.cli, "open_archive", return_value=mock_archive): + self.cli.list(None, None) + messages = ("{} {:>10} {} {}".format(fil.permissions, fil.size, fil.timestamp, fil.filename) + for fil in archive_attrs["files"].values()) + logs = (("pysap.pysapcar", "INFO", message) for message in messages) + self.log.check_present(*logs, order_matters=False) + + def test_append_no_args(self): + self.cli.append(None, []) + self.log.check_present(("pysap.pysapcar", "ERROR", "pysapcar: no files specified for appending")) + + def test_append_open_fails(self): + with mock.patch.object(self.cli, "open_archive", return_value=None): + self.assertIsNone(self.cli.append(None, ["blah"])) + self.log.check() # Check that there's no log messages + + def test_append_no_renaming(self): + names = ["list", "of", "test", "names"] + mock_sar = mock.Mock(spec=SAPCARArchive) + # Because of pass by reference and lazy string formatting in logging, we can't actually check that args are + # being printed correctly. Or we could if we logged a copy of args or str(args) in production code, but it would + # beat the point of pass by reference and lazy string formatting, so let's not. Left the generator here because + # it's pretty in it's obscurity. And because someone (probably future me) will probably bang one's head + # against the same wall I just did before coming to the same realization I just did, so here's a little + # something for the next poor soul. May it make your head-against-the-wall session shorter than mine was. + # debugs = (("pysap.pysapcar", "DEBUG", str(names[i + 1:])) for i in range(len(names))) + infos = (("pysap.pysapcar", "INFO", "d {}".format(name)) for name in names) + calls = [mock.call.add_file(name, archive_filename=name) for name in names] + calls.append(mock.call.write()) + with mock.patch.object(self.cli, "open_archive", return_value=mock_sar): + # Pass copy of names so generator works correctly + self.cli.append(None, names[:]) + # self.log.check_present(*debugs) + self.log.check_present(*infos) + # For some reason mock_sar.assert_has_calls(calls) fails, even though this passes... + self.assertEqual(calls, mock_sar.mock_calls) + + def test_append_with_renaming(self): + names = ["test", "/n", "blah", "test", "test", "/n", "blah2"] + archive_names = [("test", "blah"), ("test", "blah2")] + # List instead of generator, as we need to insert one line + infos = [("pysap.pysapcar", "INFO", "d {} (original name {})".format(archive_name, name)) + for name, archive_name in archive_names] + infos.insert(1, ("pysap.pysapcar", "INFO", "d {}".format("test"))) + mock_sar = mock.Mock(spec=SAPCARArchive) + calls = [mock.call.add_file(name, archive_filename=archive_name) for name, archive_name in archive_names] + calls.insert(1, mock.call.add_file("test", archive_filename="test")) + calls.append(mock.call.write()) + with mock.patch.object(self.cli, "open_archive", return_value=mock_sar): + self.cli.append(None, names[:]) + self.log.check_present(*infos) + # For some reason mock_sar.assert_has_calls(calls) fails, even though this passes... + self.assertEqual(calls, mock_sar.mock_calls) + + def test_extract_open_fails(self): + with mock.patch.object(self.cli, "open_archive", return_value=None): + self.assertIsNone(self.cli.extract(None, None)) + self.log.check() # Check that there's no log messages + + def test_extract_empty_archive(self): + with mock.patch.object(self.cli, "open_archive", return_value=self.mock_archive): + with mock.patch.object(self.cli, "target_files", return_value=[]): + self.cli.extract(None, None) + self.log.check(("pysap.pysapcar", "INFO", "pysapcar: 0 file(s) processed")) + + def test_extract_invalid_file_type(self): + key = self.mock_file.filename + self.mock_file.is_file = lambda: False + self.mock_file.is_directory = lambda: False + with mock.patch.object(self.cli, "open_archive", return_value=self.mock_archive): + with mock.patch.object(self.cli, "target_files", return_value=[key]): + key = key.replace("\x00", "") + self.cli.extract(mock.MagicMock(outdir=False), None) + self.log.check( + ("pysap.pysapcar", "WARNING", "pysapcar: Invalid file type '{}'".format(key)), + ("pysap.pysapcar", "INFO", "pysapcar: 0 file(s) processed") + ) + + @mock.patch.multiple("pysap.sapcarcli", autospec=True, utime=mock.DEFAULT, chmod=mock.DEFAULT, + makedirs=mock.DEFAULT) + def test_extract_dir_exists(self, makedirs, chmod, utime): + makedirs.side_effect = OSError(17, "Unit test error") + key = self.mock_dir.filename + with mock.patch.object(self.cli, "open_archive", return_value=self.mock_archive): + with mock.patch.object(self.cli, "target_files", return_value=[key]): + key = key.replace("\x00", "") + self.cli.extract(mock.MagicMock(outdir=False), None) + makedirs.assert_called_once_with(key) + chmod.assert_not_called() + utime.assert_not_called() + self.log.check( + ("pysap.pysapcar", "INFO", "d {}".format(key)), + ("pysap.pysapcar", "INFO", "pysapcar: 1 file(s) processed") + ) + + @mock.patch.multiple("pysap.sapcarcli", autospec=True, utime=mock.DEFAULT, chmod=mock.DEFAULT, + makedirs=mock.DEFAULT) + def test_extract_dir_creation_fails(self, makedirs, chmod, utime): + makedirs.side_effect = OSError(13, "Unit test error") + key = self.mock_dir.filename + with mock.patch.object(self.cli, "open_archive", return_value=self.mock_archive): + with mock.patch.object(self.cli, "target_files", return_value=[key]): + key = key.replace("\x00", "") + self.cli.extract(mock.MagicMock(outdir=False), None) + makedirs.assert_called_once_with(key) + chmod.assert_not_called() + utime.assert_not_called() + self.log.check( + ("pysap.pysapcar", "ERROR", "pysapcar: Could not create directory '{}' ([Errno 13] Unit test error)" + .format(key)), + ("pysap.pysapcar", "INFO", "pysapcar: Stopping extraction"), + ("pysap.pysapcar", "INFO", "pysapcar: 0 file(s) processed") + ) + + @mock.patch.multiple("pysap.sapcarcli", autospec=True, utime=mock.DEFAULT, chmod=mock.DEFAULT, + makedirs=mock.DEFAULT) + def test_extract_dir_passes(self, makedirs, chmod, utime): + key = self.mock_dir.filename + with mock.patch.object(self.cli, "open_archive", return_value=self.mock_archive): + with mock.patch.object(self.cli, "target_files", return_value=[key]): + key = key.replace("\x00", "") + self.cli.extract(mock.MagicMock(outdir=False), None) + makedirs.assert_called_once_with(key) + chmod.assert_called_once_with(key, self.mock_dir.perm_mode) + utime.assert_called_once_with(key, (self.mock_dir.timestamp_raw, self.mock_dir.timestamp_raw)) + self.log.check( + ("pysap.pysapcar", "INFO", "d {}".format(key)), + ("pysap.pysapcar", "INFO", "pysapcar: 1 file(s) processed") + ) + + @mock.patch.multiple("pysap.sapcarcli", autospec=True, utime=mock.DEFAULT, fchmod=mock.DEFAULT, + makedirs=mock.DEFAULT) + def test_extract_file_intermediate_dir_exists(self, makedirs, fchmod, utime): + makedirs.side_effect = OSError(17, "Unit test error") + key = path.join("test", "bl\x00ah") + mock_file = mock.Mock(spec=SAPCARArchiveFile, is_directory=lambda: False, perm_mode="-rw-rw-r--", + timestamp_raw=7) + mock_arch = mock.Mock(spec=SAPCARArchive, files={key: mock_file}) + with mock.patch.object(self.cli, "open_archive", return_value=mock_arch): + with mock.patch.object(self.cli, "target_files", return_value=[key]): + key = key.replace("\x00", "") + with mock.patch("pysap.sapcarcli.open", mock.mock_open()) as mock_open: + mock_open.return_value.fileno.return_value = 1337 # yo dawg... + self.cli.extract(mock.MagicMock(outdir=False), None) + dirname = path.dirname(key) + makedirs.assert_called_once_with(dirname) + fchmod.assert_called_once_with(1337, "-rw-rw-r--") + utime.assert_called_once_with(key, (7, 7)) + self.log.check( + ("pysap.pysapcar", "INFO", "d {}".format(key)), + ("pysap.pysapcar", "INFO", "pysapcar: 1 file(s) processed") + ) + + @mock.patch.multiple("pysap.sapcarcli", autospec=True, utime=mock.DEFAULT, fchmod=mock.DEFAULT, + makedirs=mock.DEFAULT) + def test_extract_file_intermediate_dir_creation_fails(self, makedirs, fchmod, utime): + key = self.mock_file.filename + makedirs.side_effect = OSError(13, "Unit test error") + with mock.patch.object(self.cli, "open_archive", return_value=self.mock_archive): + with mock.patch.object(self.cli, "target_files", return_value=[key]): + key = key.replace("\x00", "") + self.cli.extract(mock.MagicMock(outdir=False), None) + dirname = path.dirname(key) + makedirs.assert_called_once_with(dirname) + fchmod.assert_not_called() + utime.assert_not_called() + self.log.check( + ("pysap.pysapcar", "ERROR", "pysapcar: Could not create intermediate directory '{}' for '{}' " + "(Unit test error)".format(dirname, key)), + ("pysap.pysapcar", "INFO", "pysapcar: Stopping extraction"), + ("pysap.pysapcar", "INFO", "pysapcar: 0 file(s) processed") + ) + + @mock.patch.multiple("pysap.sapcarcli", autospec=True, utime=mock.DEFAULT, fchmod=mock.DEFAULT, + makedirs=mock.DEFAULT) + def test_extract_file_passes(self, makedirs, fchmod, utime): + key = self.mock_file.filename + with mock.patch.object(self.cli, "open_archive", return_value=self.mock_archive): + with mock.patch.object(self.cli, "target_files", return_value=[key]): + key = key.replace("\x00", "") + with mock.patch("pysap.sapcarcli.open", mock.mock_open()) as mock_open: + mock_open.return_value.fileno.return_value = 1337 # yo dawg... + self.cli.extract(mock.MagicMock(outdir=False), None) + dirname = path.dirname(key) + makedirs.assert_called_once_with(dirname) + fchmod.assert_called_once_with(1337, "-rw-rw-r--") + utime.assert_called_once_with(key, (7, 7)) + self.log.check( + ("pysap.pysapcar", "INFO", "d {}".format(dirname)), + ("pysap.pysapcar", "INFO", "d {}".format(key)), + ("pysap.pysapcar", "INFO", "pysapcar: 1 file(s) processed") + ) + + @mock.patch.multiple("pysap.sapcarcli", autospec=True, utime=mock.DEFAULT, fchmod=mock.DEFAULT) + def test_extract_file_invalid_file(self, fchmod, utime): + key = self.mock_file.filename + self.mock_file.open.side_effect = SAPCARInvalidFileException("Unit test error") + with mock.patch.object(self.cli, "open_archive", return_value=self.mock_archive): + with mock.patch.object(self.cli, "target_files", return_value=[key]): + self.cli.extract(mock.MagicMock(outdir=False, break_on_error=False), None) + fchmod.assert_not_called() + utime.assert_not_called() + self.log.check( + ("pysap.pysapcar", "ERROR", "pysapcar: Invalid SAP CAR file '{}' (Unit test error)" + .format(self.cli.archive_fd.name)), + ("pysap.pysapcar", "INFO", "pysapcar: Skipping extraction of file '{}'".format(key)), + ("pysap.pysapcar", "INFO", "pysapcar: 0 file(s) processed") + ) + + @mock.patch.multiple("pysap.sapcarcli", autospec=True, utime=mock.DEFAULT, fchmod=mock.DEFAULT) + def test_extract_file_invalid_checksum(self, fchmod, utime): + key = self.mock_file.filename + self.mock_file.open.side_effect = SAPCARInvalidChecksumException("Unit test error") + with mock.patch.object(self.cli, "open_archive", return_value=self.mock_archive): + with mock.patch.object(self.cli, "target_files", return_value=[key]): + self.cli.extract(mock.MagicMock(outdir=False, break_on_error=False), None) + fchmod.assert_not_called() + utime.assert_not_called() + self.log.check( + ("pysap.pysapcar", "ERROR", "pysapcar: Invalid checksum found for file '{}'" + .format(key)), + ("pysap.pysapcar", "INFO", "pysapcar: Stopping extraction"), + ("pysap.pysapcar", "INFO", "pysapcar: 0 file(s) processed") + ) + + @mock.patch.multiple("pysap.sapcarcli", autospec=True, utime=mock.DEFAULT, fchmod=mock.DEFAULT) + def test_extract_file_extraction_fails(self, fchmod, utime): + key = self.mock_file.filename + with mock.patch.object(self.cli, "open_archive", return_value=self.mock_archive): + with mock.patch.object(self.cli, "target_files", return_value=[key]): + key = key.replace("\x00", "") + with mock.patch("pysap.sapcarcli.open", mock.mock_open()) as mock_open: + mock_open.side_effect = OSError(13, "Unit test error") + self.cli.extract(mock.MagicMock(outdir=False), None) + dirname = path.dirname(key) + fchmod.assert_not_called() + utime.assert_not_called() + self.log.check( + ("pysap.pysapcar", "INFO", "d {}".format(dirname)), + ("pysap.pysapcar", "ERROR", "pysapcar: Failed to extract file '{}', ([Errno 13] Unit test error)".format(key)), + ("pysap.pysapcar", "INFO", "pysapcar: Stopping extraction"), + ("pysap.pysapcar", "INFO", "pysapcar: 0 file(s) processed") + ) + + +def test_suite(): + loader = unittest.TestLoader() + suite = unittest.TestSuite() + suite.addTest(loader.loadTestsFromTestCase(PySAPCARCLITest)) + return suite + + +if __name__ == "__main__": + unittest.TextTestRunner(verbosity=2).run(test_suite()) diff --git a/tests/sapcredv2_test.py b/tests/test_sapcredv2.py old mode 100644 new mode 100755 similarity index 96% rename from tests/sapcredv2_test.py rename to tests/test_sapcredv2.py index 33a8a5f..e343e5a --- a/tests/sapcredv2_test.py +++ b/tests/test_sapcredv2.py @@ -31,9 +31,11 @@ class PySAPCredV2Test(unittest.TestCase): decrypt_username = "username" - decrypt_pin = "1234567890" - cert_name = "CN=PSEOwner" - common_name = "PSEOwner" + decrypt_pin = b"1234567890" + cert_name = b"CN=PSEOwner" + common_name = b"PSEOwner" + pse_path = b"/secudir/pse-v2-noreq-DSA-1024-SHA1.pse" + pse_path_win = b"C:\\secudir\\pse-v2-noreq-DSA-1024-SHA1.pse" subject_str = "/CN=PSEOwner" subject = [ X509_RDN(rdn=[ @@ -41,8 +43,6 @@ class PySAPCredV2Test(unittest.TestCase): value=ASN1_PRINTABLE_STRING(common_name)) ]) ] - pse_path = "/secudir/pse-v2-noreq-DSA-1024-SHA1.pse" - pse_path_win = "C:\\secudir\\pse-v2-noreq-DSA-1024-SHA1.pse" def validate_credv2_lps_off_fields(self, creds, number, lps_type, cipher_format_version, cipher_algorithm, cert_name=None, pse_path=None): @@ -56,9 +56,9 @@ def validate_credv2_lps_off_fields(self, creds, number, lps_type, cipher_format_ self.assertEqual(cred.cipher_algorithm, cipher_algorithm) self.assertEqual(cred.cert_name, cert_name or self.cert_name) - self.assertEqual(cred.unknown1, "") + self.assertEqual(cred.unknown1, b"") self.assertEqual(cred.pse_path, pse_path or self.pse_path) - self.assertEqual(cred.unknown2, "") + self.assertEqual(cred.unknown2, b"") def validate_credv2_plain(self, cred, decrypt_username=None, decrypt_pin=None): plain = cred.decrypt(decrypt_username or self.decrypt_username) @@ -85,6 +85,7 @@ def test_credv2_lps_off_v0_dp_3des(self): with open(data_filename("credv2_lps_off_v0_dp_3des"), "rb") as fd: s = fd.read() creds = SAPCredv2(s).creds + self.validate_credv2_lps_off_fields(creds, 1, None, 0, CIPHER_ALGORITHM_3DES, pse_path=self.pse_path_win) diff --git a/tests/sapdiag_test.py b/tests/test_sapdiag.py similarity index 100% rename from tests/sapdiag_test.py rename to tests/test_sapdiag.py diff --git a/tests/saphdb_test.py b/tests/test_saphdb.py similarity index 94% rename from tests/saphdb_test.py rename to tests/test_saphdb.py index 204a45d..186da48 100755 --- a/tests/saphdb_test.py +++ b/tests/test_saphdb.py @@ -20,7 +20,8 @@ import sys import unittest from threading import Thread -from SocketServer import BaseRequestHandler, ThreadingTCPServer +# External imports +from six.moves.socketserver import BaseRequestHandler, ThreadingTCPServer # Custom imports from pysap.SAPHDB import SAPHDBConnection @@ -30,7 +31,7 @@ class SAPHDBServerTestHandler(BaseRequestHandler): def handle_data(self): self.request.recv(14) - self.request.send("\x00" * 8) + self.request.send(b"\x00" * 8) class PySAPHDBConnectionTest(unittest.TestCase): diff --git a/tests/sapni_test.py b/tests/test_sapni.py similarity index 96% rename from tests/sapni_test.py rename to tests/test_sapni.py index 1757174..f2acecb 100755 --- a/tests/sapni_test.py +++ b/tests/test_sapni.py @@ -22,7 +22,7 @@ import unittest from threading import Thread from struct import pack, unpack -from SocketServer import BaseRequestHandler, ThreadingTCPServer +from six.moves.socketserver import BaseRequestHandler, ThreadingTCPServer # External imports from scapy.fields import StrField from scapy.packet import Packet, Raw @@ -53,13 +53,13 @@ def stop_server(self): class PySAPNITest(unittest.TestCase): - test_string = "LALA" * 10 + test_string = b"LALA" * 10 def test_sapni_building(self): """Test SAPNI length field building""" sapni = SAPNI() / self.test_string - (sapni_length, ) = unpack("!I", str(sapni)[:4]) + (sapni_length, ) = unpack("!I", bytes(sapni)[:4]) self.assertEqual(sapni_length, len(self.test_string)) self.assertEqual(sapni.payload.load, self.test_string) @@ -91,21 +91,21 @@ class SAPNITestHandlerKeepAlive(SAPNITestHandler): def handle(self): SAPNITestHandler.handle(self) - self.request.sendall("\x00\x00\x00\x08NI_PING\x00") + self.request.sendall(b"\x00\x00\x00\x08NI_PING\x00") class SAPNITestHandlerClose(SAPNITestHandler): """Basic SAP NI server that closes the connection""" def handle(self): - self.request.send("") + self.request.send(b"") class PySAPNIStreamSocketTest(PySAPBaseServerTest): test_port = 8005 test_address = "127.0.0.1" - test_string = "TEST" * 10 + test_string = b"TEST" * 10 def test_sapnistreamsocket(self): """Test SAPNIStreamSocket""" @@ -239,7 +239,7 @@ class PySAPNIServerTest(PySAPBaseServerTest): test_port = 8005 test_address = "127.0.0.1" - test_string = "TEST" * 10 + test_string = b"TEST" * 10 handler_cls = SAPNIServerTestHandler def test_sapniserver(self): @@ -268,7 +268,7 @@ class PySAPNIProxyTest(PySAPBaseServerTest): test_proxyport = 8005 test_serverport = 8006 test_address = "127.0.0.1" - test_string = "TEST" * 10 + test_string = b"TEST" * 10 proxyhandler_cls = SAPNIProxyHandler serverhandler_cls = SAPNIServerTestHandler @@ -326,7 +326,7 @@ def process_server(self, packet): sock.connect((self.test_address, self.test_proxyport)) sock.sendall(pack("!I", len(self.test_string)) + self.test_string) - expected_reponse = self.test_string + "Client" + "Server" + expected_reponse = self.test_string + b"Client" + b"Server" response = sock.recv(4) self.assertEqual(len(response), 4) diff --git a/tests/sappse_test.py b/tests/test_sappse.py old mode 100644 new mode 100755 similarity index 96% rename from tests/sappse_test.py rename to tests/test_sappse.py index fa40a4b..17eb87b --- a/tests/sappse_test.py +++ b/tests/test_sappse.py @@ -20,6 +20,7 @@ import sys import unittest # External imports +from six import b, assertRaisesRegex # Custom imports from tests.utils import data_filename from pysap.SAPPSE import (SAPPSEFile, PKCS12_ALGORITHM_PBE1_SHA_3DES_CBC) @@ -51,7 +52,7 @@ def test_pse_v2_lps_off_pbes1_3des_sha1_decrypt(self): s = fd.read() pse = SAPPSEFile(s) - self.assertRaisesRegexp(ValueError, "Invalid PIN supplied", pse.decrypt, "Some Invalid PIN") + assertRaisesRegex(self, ValueError, "Invalid PIN supplied", pse.decrypt, "Some Invalid PIN") pse.decrypt(self.decrypt_pin) def test_pse_v4_lps_off_pbes1_3des_sha1(self): diff --git a/tests/saprouter_test.py b/tests/test_saprouter.py similarity index 99% rename from tests/saprouter_test.py rename to tests/test_saprouter.py index 8b13f9b..806f02e 100755 --- a/tests/saprouter_test.py +++ b/tests/test_saprouter.py @@ -219,7 +219,7 @@ def test_saproutedstreamsocket_getnisocket(self): route = [SAPRouterRouteHop(hostname=self.test_address, port=self.test_port)] self.client = SAPRoutedStreamSocket.get_nisocket("10.0.0.1", - "3200", + 3200, route=route, router_version=40) diff --git a/tests/sapssfs_test.py b/tests/test_sapssfs.py similarity index 85% rename from tests/sapssfs_test.py rename to tests/test_sapssfs.py index 878cc6c..84eff86 100755 --- a/tests/sapssfs_test.py +++ b/tests/test_sapssfs.py @@ -19,15 +19,16 @@ # Standard imports import unittest # External imports +from six import b # Custom imports from tests.utils import data_filename -from pysap.SAPSSFS import (SAPSSFSKey, SAPSSFSData, SAPSSFSLock) +from pysap.SAPSSFS import (SAPSSFSKey, SAPSSFSData) class PySAPSSFSKeyTest(unittest.TestCase): - USERNAME = "SomeUser " - HOST = "ubuntu " + USERNAME = b("SomeUser ") + HOST = b("ubuntu ") def test_ssfs_key_parsing(self): """Test parsing of a SSFS Key file""" @@ -37,7 +38,7 @@ def test_ssfs_key_parsing(self): key = SAPSSFSKey(s) - self.assertEqual(key.preamble, "RSecSSFsKey") + self.assertEqual(key.preamble, b("RSecSSFsKey")) self.assertEqual(key.type, 1) self.assertEqual(key.user, self.USERNAME) self.assertEqual(key.host, self.HOST) @@ -45,12 +46,12 @@ def test_ssfs_key_parsing(self): class PySAPSSFSDataTest(unittest.TestCase): - USERNAME = "SomeUser " - HOST = "ubuntu " + USERNAME = b("SomeUser ") + HOST = b("ubuntu ") - PLAIN_VALUES = {"HDB/KEYNAME/DB_CON_ENV": "Env", - "HDB/KEYNAME/DB_DATABASE_NAME": "Database", - "HDB/KEYNAME/DB_USER": "SomeUser", + PLAIN_VALUES = {b("HDB/KEYNAME/DB_CON_ENV"): b("Env"), + b("HDB/KEYNAME/DB_DATABASE_NAME"): b("Database"), + b("HDB/KEYNAME/DB_USER"): b("SomeUser"), } def test_ssfs_data_parsing(self): @@ -63,7 +64,7 @@ def test_ssfs_data_parsing(self): self.assertEqual(len(data.records), 4) for record in data.records: - self.assertEqual(record.preamble, "RSecSSFsData") + self.assertEqual(record.preamble, b("RSecSSFsData")) self.assertEqual(record.length, len(record)) self.assertEqual(record.type, 1) self.assertEqual(record.user, self.USERNAME) @@ -101,21 +102,21 @@ def test_ssfs_data_record_hmac(self): # Now tamper with the header original_user = record.user - record.user = "NewUser" + record.user = b("NewUser") self.assertFalse(record.valid) record.user = original_user self.assertTrue(record.valid) # Now tamper with the data orginal_data = record.data - record.data = orginal_data + "AddedDataBytes" + record.data = orginal_data + b("AddedDataBytes") self.assertFalse(record.valid) record.data = orginal_data self.assertTrue(record.valid) # Now tamper with the HMAC orginal_hmac = record.hmac - record.hmac = orginal_hmac[:-1] + "A" + record.hmac = orginal_hmac[:-1] + b("A") self.assertFalse(record.valid) record.hmac = orginal_hmac self.assertTrue(record.valid) @@ -123,7 +124,7 @@ def test_ssfs_data_record_hmac(self): class PySAPSSFSDataDecryptTest(unittest.TestCase): - ENCRYPTED_VALUES = {"HDB/KEYNAME/DB_PASSWORD": "SomePassword"} + ENCRYPTED_VALUES = {b("HDB/KEYNAME/DB_PASSWORD"): b("SomePassword")} def test_ssfs_data_record_decrypt(self): """Test decrypting a record with a given key in a SSFS Data file.""" diff --git a/tests/utils.py b/tests/utils.py index 36e97d0..ec944fa 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -18,19 +18,19 @@ # Standard imports from binascii import unhexlify -from os.path import join as join, dirname +from os import path def data_filename(filename): - return join(dirname(__file__), 'data', filename) + return path.join(path.dirname(__file__), 'data', filename) def read_data_file(filename, unhex=True): filename = data_filename(filename) - with open(filename, 'r') as f: + with open(filename, 'rb') as f: data = f.read() - data = data.replace('\n', ' ').replace(' ', '') + data = data.replace(b'\n', b' ').replace(b' ', b'') if unhex: data = unhexlify(data) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..d88b43c --- /dev/null +++ b/tox.ini @@ -0,0 +1,15 @@ +[tox] +envlist = py27,py36,py37,py38 + +[testenv] +basepython = + py27: python2.7 + py36: python3.6 + py37: python3.7 + py38: python3.8 + py39: python3.9 +deps=-rrequirements.txt + -rrequirements-tests.txt +passenv = NO_REMOTE +commands_pre = {envpython} -m pip check +commands = {envpython} -m pytest {posargs} \ No newline at end of file pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy